Compare commits

..
2 Commits
Author SHA1 Message Date
js6pak 3e857ed555 Fix plugin host disposal 2021-06-13 21:35:04 +02:00
js6pak 9d31752bf0 Add PluginManager 2021-06-12 15:51:39 +02:00
75 changed files with 4171 additions and 453 deletions

No files matched your search

+36 -6
View File
@@ -3,11 +3,14 @@
[![Discord](https://img.shields.io/badge/Discord-chat-blue?style=flat-square)](https://discord.gg/Mk3w6Tb)
[![AppVeyor](https://img.shields.io/appveyor/build/Impostor/Impostor/master?style=flat-square)](https://ci.appveyor.com/project/Impostor/Impostor/branch/master)
Impostor is the first working **Among Us** private server, written in C#.
Impostor is one of the first **Among Us** private servers, written in C#.
As Among Us is actively updated, you may want to use the latest build from the master branch if you want to play on the latest version of Among Us that supported by Impostor.
If you want to play on an older version of Among Us, you can download old releases from [here](https://github.com/Impostor/Impostor/releases/), however these versions are less supported.
| Impostor version | Among Us version | Experimental | Download |
|-|-|-|-|
| 1.1.0 | 2020.09.07 - 2020.09.22 | No | [![Download](https://img.shields.io/badge/Download-v1.1.0-blue?style=flat-square)](https://github.com/Impostor/Impostor/releases/tag/v1.1.0) |
| 1.2.2 | 2020.09.22 - 2020.11.17 | No | [![Download](https://img.shields.io/badge/Download-v1.2.2-blue?style=flat-square)](https://github.com/Impostor/Impostor/releases/tag/v1.2.2) |
| 1.3.0 | 2021.3.5 | No | [![Download](https://img.shields.io/badge/Download-v1.3.0-blue?style=flat-square)](https://github.com/Impostor/Impostor/releases/tag/v1.3.0) |
| 1.4.0 | 2021.3.31 - 2021.4.2 | Yes | [![Download](https://img.shields.io/badge/Download-master-blue?style=flat-square)](https://ci.appveyor.com/project/Impostor/Impostor/branch/master/artifacts) |
## Features
@@ -19,7 +22,34 @@ If you want to play on an older version of Among Us, you can download old releas
### Client
If you just want to play on a server hosted by someone else, you need to configure your client to connect to an Impostor server. Please go to [this website](https://impostor.github.io/Impostor) to set up your client by following the instructions for your OS there.
If you just want to play on a server hosted by someone else, you need to follow these instructions.
#### Windows
1. Find the [latest release](https://github.com/AeonLucid/Impostor/releases/latest).
2. Download `Impostor-Patcher-win-x64.zip`.
3. Extract the zip.
4. Run `Impostor.Patcher.exe`.
5. Follow the instructions inside the application.
![Client](docs/images/client.jpg)
If you do not wish to execute any programs. Follow the instructions in [this website](https://impostor.github.io/Impostor)
#### Android
##### Android 10 and below.
1. Go to [this website](https://impostor.github.io/Impostor) **(ON YOUR MOBILE DEVICE)**
2. Follow the instructions listed there.
##### Android 11.
1. Connect your phone to a computer. Go to [this website](https://impostor.github.io/Impostor) on the computer and follow the steps 1 and 2 to generate a `regionInfo.dat` file.
2. Instead of following the next steps, open the phone's internal storage on your computer and navigate to `/sdcard/Android/data/com.innersloth.spacemafia/files`.
3. Copy the generated `regionInfo.dat` file into the `files` folder you just navigated to.
#### iOS
iOS devices need to be jailbroken in order to connect to Impostor servers.
### Server
@@ -27,7 +57,7 @@ See the [docs](docs/Running-the-server.md) for instructions on how to set it up.
## Troubleshooting
See [TROUBLESHOOTING](docs/TROUBLESHOOTING.md) to solve issues with the Impostor client or the server.
See [TROUBLESHOOTING](docs/TROUBLESHOOTING.md) to solve issues the Impostor client or the server.
## Contributing
+7
View File
@@ -114,6 +114,13 @@ Task("Build")
DotNetCoreBuild("./src/Impostor.Tests/Impostor.Tests.csproj", new DotNetCoreBuildSettings {
Configuration = configuration,
});
// Client.
ImpostorPublishNF("Impostor-Patcher", "./src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj");
ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "win-x64");
ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "osx-x64");
ImpostorPublish("Impostor-Patcher-Cli", "./src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj", "linux-x64");
// Server.
ImpostorPublish("Impostor-Server", "./src/Impostor.Server/Impostor.Server.csproj", "win-x64", true);
+14 -1
View File
@@ -1,11 +1,13 @@
# Building from source
The solution contains two main projects, the Impostor server and patcher. The server is built using [.NET 5](https://dotnet.microsoft.com/download/dotnet/5.0) and the winforms patcher is with [.NET Framework 4.7.2](https://dotnet.microsoft.com/download/dotnet-framework/net472).
## Cloning Impostor
You need to clone Impostor using git.
```bash
git clone https://github.com/Impostor/Impostor.git
git clone https://github.com/AeonLucid/Impostor.git
```
## Building the server
@@ -21,3 +23,14 @@ cd src/Impostor.Server/
dotnet build
```
To setup the server, please look at [Running the server](Running-the-server.md).
## Building the winforms patcher
### Dependencies
* [.NET Framework 4.7.2 Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net472-developer-pack-offline-installer) or [Mono](https://www.mono-project.com/download/)
### Build using the CLI
```bash
cd src/Impostor.Patcher/Impostor.Patcher.WinForms
dotnet build
```
+5 -5
View File
@@ -6,14 +6,14 @@ There are currently two modes to run the Impostor server in. The first way is th
### Without docker
1. Install the **.NET 5.0 runtime**.
- [Windows x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.7-windows-x64-installer)
- [Windows x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.0-windows-x64-installer)
- [Linux x64](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
- [macOS x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.7-macos-x64-installer)
2. Find the [latest release](https://github.com/Impostor/Impostor/releases) or the [latest CI build](https://ci.appveyor.com/project/Impostor/Impostor/branch/master/artifacts).
3. Download the version for your OS (linux/win/osx) and your architecture (x64/arm/arm64).
- [macOS x64](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-5.0.0-macos-x64-installer)
2. Find the [latest master release](https://ci.appveyor.com/project/Impostor/Impostor/branch/master/artifacts).
3. Download either the Windows or the Linux version.
4. Extract the zip.
5. Modify `config.json` to your liking. Documentation can be found [here](Server-configuration.md) *(this step is mandatory if you want to expose this server to other devices)*
6. Run `Impostor.Server` (Linux/macOS) or `Impostor.Server.exe` (Windows)
6. Run `Impostor.Server.exe` (Windows) / `Impostor.Server` (Linux)
### Using docker
+8 -4
View File
@@ -31,11 +31,12 @@ The first step is creating a new C# project, it must be a **Class Library (.NET
- .NET Standard 2.0
- .NET Standard 2.1
- .NET Core 3.1
- .NET 5
For more information about compatibility, see https://docs.microsoft.com/en-us/dotnet/standard/net-standard.
> At the moment of writing this document, I recommend you to use **.NET Standard 2.1**. This should give you enough functionality. If not, upgrade to .NET 5.
> At the moment of writing this document, I recommend you to use **.NET Standard 2.1** until .NET 5 is released officially. This should give you enough functionality. If not, upgrade to .NET Core 3.1.
When the project has been created, you should have `Class.cs` and `Project.csproj` files. Your `Project.csproj` should look something like this.
@@ -61,13 +62,16 @@ Make sure to get a prerelease if you are writing a plugin for a dev release of t
1. Right click your project.
2. Click `Manage NuGet Packages`.
3. Click `Browse`.
4. Search for `Impostor.Api`.
5. Click the `Impostor.Api` result and press install on the right side.
4. Next to the search bar, enable `Include prerelease`.
5. Search for `Impostor.Api`.
6. Click the `Impostor.Api` result and press install on the right side.
### Dotnet CLI
> Make sure to grab the latest (pre-)release version from NuGet [here](https://www.nuget.org/packages/Impostor.Api).
1. Open your project folder in command prompt / bash.
2. Run `dotnet add package Impostor.Api`.
2. Run `dotnet add package Impostor.Api -v "1.2.0-ci.58"`.
## 4. The plugin class
-6
View File
@@ -144,12 +144,6 @@ dotnet_naming_style.end_in_async.word_separator =
# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
dotnet_diagnostic.RS2008.severity = none
# IDE0005: Remove unnecessary import
dotnet_diagnostic.IDE0005.severity = warning
# IDE0007: Use `var` instead of explicit type
dotnet_diagnostic.IDE0007.severity = warning
# IDE0035: Remove unreachable code
dotnet_diagnostic.IDE0035.severity = warning
+1 -2
View File
@@ -1,8 +1,7 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.5.0</VersionPrefix>
<VersionPrefix>1.4.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
@@ -9,6 +9,11 @@ namespace Impostor.Api.Events.Announcements
{
public interface IResponse
{
/// <summary>
/// Gets or sets FreeWeekendState, currently unused by the client.
/// </summary>
public FreeWeekendState FreeWeekendState { get; set; }
/// <summary>
/// Gets or sets a value indicating whether announcement should be loaded from client's cache, can save some bytes.
/// </summary>
@@ -2,12 +2,10 @@ using Impostor.Api.Net.Inner.Objects;
namespace Impostor.Api.Events.Player
{
public enum VoteType : byte
public enum VoteType : sbyte
{
HasNotVoted = 255,
Missed = 254,
Skipped = 253,
Dead = 252,
ForceSkip = -2,
Skip = -1,
Player = 0,
}
@@ -14,11 +14,5 @@
Brown = 9,
Cyan = 10,
Lime = 11,
Maroon = 12,
Rose = 13,
Banana = 14,
Gray = 15,
Tan = 16,
Coral = 17,
}
}
@@ -1,4 +1,4 @@
namespace Impostor.Api.Innersloth.Customization
namespace Impostor.Api.Innersloth.Customization
{
public enum HatType : uint
{
@@ -109,13 +109,13 @@ namespace Impostor.Api.Innersloth.Customization
ToppatHenry = 104,
ToppatEllie = 105,
GeoffreyPlumb = 106,
RightHandMan = 107,
AngryEyebrows = 108,
ChocolateIce = 109,
HeartPin = 110,
Ponytail = 111,
RubberGlove = 112,
UnicornHorn = 113,
Zipper = 114,
AngryEyebrows = 107,
ChocolateIce = 108,
HeartPin = 109,
Ponytail = 110,
RubberGlove = 111,
UnicornHorn = 112,
Zipper = 113,
RightHandMan = 114,
}
}
@@ -0,0 +1,12 @@
using System;
namespace Impostor.Api.Innersloth
{
[Flags]
public enum FreeWeekendState : byte
{
NotFree,
FreeMIRA,
FreePolus,
}
}
+5 -12
View File
@@ -6,21 +6,14 @@ namespace Impostor.Api.Innersloth
public enum GameKeywords : uint
{
All = 0,
English = 256,
SpanishLA = 2,
Brazilian = 2048,
Portuguese = 16,
Other = 1,
Spanish = 2,
Korean = 4,
Russian = 8,
Dutch = 4096,
Filipino = 64,
French = 8192,
German = 16384,
Italian = 32768,
Japanese = 512,
SpanishEU = 1024,
Portuguese = 16,
Arabic = 32,
Filipone = 64,
Polish = 128,
Other = 1,
English = 256,
}
}
+3 -13
View File
@@ -1,20 +1,10 @@
namespace Impostor.Api.Innersloth
{
public static class GameVersion
public class GameVersion
{
public static int GetVersion(int year, int month, int day, int revision = 0)
public static int GetVersion(int year, int month, int day, int rev = 0)
{
return (year * 25000) + (month * 1800) + (day * 50) + revision;
}
public static void ParseVersion(int version, out int year, out int month, out int day, out int revision)
{
year = version / 25000;
version %= 25000;
month = version / 1800;
version %= 1800;
day = version / 50;
revision = version % 50;
return (year * 25000) + (month * 1800) + (day * 50) + rev;
}
}
}
+1 -9
View File
@@ -3,17 +3,9 @@ namespace Impostor.Api.Innersloth
public enum Language
{
English,
SpanishLA,
Brazilian,
Spanish,
Portuguese,
Korean,
Russian,
Dutch,
Filipino,
French,
German,
Italian,
Japanese,
SpanishEU,
}
}
+50
View File
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.IO;
namespace Impostor.Api.Innersloth
{
public class RegionInfo
{
public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers)
{
Name = name;
Ping = ping;
Servers = servers;
}
public string Name { get; }
public string Ping { get; }
public IReadOnlyList<ServerInfo> Servers { get; }
public static RegionInfo Deserialize(BinaryReader reader)
{
var unknown = reader.ReadInt32();
var name = reader.ReadString();
var ping = reader.ReadString();
var servers = new List<ServerInfo>();
var serverCount = reader.ReadInt32();
for (var i = 0; i < serverCount; i++)
{
servers.Add(ServerInfo.Deserialize(reader));
}
return new RegionInfo(name, ping, servers);
}
public void Serialize(BinaryWriter writer)
{
writer.Write(0);
writer.Write(Name);
writer.Write(Ping);
writer.Write(Servers.Count);
foreach (var server in Servers)
{
server.Serialize(writer);
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
using System.IO;
using System.Net;
namespace Impostor.Api.Innersloth
{
public class ServerInfo
{
public ServerInfo(string name, string ip, ushort port)
{
Name = name;
Ip = ip;
Port = port;
}
public string Name { get; }
public string Ip { get; }
public ushort Port { get; }
public static ServerInfo Deserialize(BinaryReader reader)
{
var name = reader.ReadString();
var ip = new IPAddress(reader.ReadBytes(4)).ToString();
var port = reader.ReadUInt16();
var unknown = reader.ReadInt32();
return new ServerInfo(name, ip, port);
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Name);
writer.Write(IPAddress.Parse(Ip).GetAddressBytes());
writer.Write(Port);
writer.Write(0);
}
}
}
-1
View File
@@ -34,6 +34,5 @@
SetTasks = 29,
ClimbLadder = 31,
UsePlatform = 32,
SendQuickChat = 33,
}
}
@@ -0,0 +1,19 @@
using Impostor.Api.Innersloth;
namespace Impostor.Api.Net.Messages.Announcements
{
public static class Message02SetFreeWeekend
{
public static void Serialize(IMessageWriter writer, FreeWeekendState state)
{
writer.StartMessage(AnnouncementsMessageFlags.SetFreeWeekend);
writer.Write((byte)state);
writer.EndMessage();
}
public static void Deserialize(IMessageReader reader, out FreeWeekendState state)
{
state = (FreeWeekendState)reader.ReadByte();
}
}
}
@@ -1,32 +0,0 @@
using System.Collections.Generic;
namespace Impostor.Api.Net.Messages.Announcements
{
public static class Message04AddLanguages
{
public static void Serialize(IMessageWriter writer, Dictionary<string, uint> languages)
{
writer.StartMessage(AnnouncementsMessageFlags.AddLanguages);
writer.Write((byte)languages.Count);
foreach (var (name, id) in languages)
{
writer.Write(name);
writer.Write(id);
}
writer.EndMessage();
}
public static void Deserialize(IMessageReader reader, out Dictionary<string, uint> languages)
{
var length = reader.ReadByte();
languages = new Dictionary<string, uint>(length);
for (var i = 0; i < length; i++)
{
languages[reader.ReadString()] = reader.ReadUInt32();
}
}
}
}
@@ -4,6 +4,6 @@ namespace Impostor.Api.Net.Messages
{
public const byte UseCache = 0;
public const byte SetUpdate = 1;
public const byte AddLanguages = 4;
public const byte SetFreeWeekend = 2;
}
}
@@ -1,3 +1,5 @@
using System;
namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc23VotingComplete
@@ -9,15 +11,9 @@ namespace Impostor.Api.Net.Messages.Rpcs
writer.Write(tie);
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie)
public static void Deserialize(IMessageReader reader, out ReadOnlyMemory<byte> states, out byte playerId, out bool tie)
{
var length = reader.ReadPackedInt32();
states = new IMessageReader[length];
for (var i = 0; i < length; i++)
{
states[i] = reader.ReadMessage();
}
states = reader.ReadBytesAndSize();
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
}
@@ -8,10 +8,10 @@ namespace Impostor.Api.Net.Messages.Rpcs
writer.Write(suspectPlayerId);
}
public static void Deserialize(IMessageReader reader, out byte playerId, out byte suspectPlayerId)
public static void Deserialize(IMessageReader reader, out byte playerId, out sbyte suspectPlayerId)
{
playerId = reader.ReadByte();
suspectPlayerId = reader.ReadByte();
suspectPlayerId = reader.ReadSByte();
}
}
}
@@ -0,0 +1,42 @@
using System;
namespace Impostor.Api.Plugins
{
public interface IPluginInformation
{
/// <summary>
/// Gets the unique id of this plugin.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the human readable name of this plugin.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the author of this plugin.
/// </summary>
string Author { get; }
/// <summary>
/// Gets the version of this plugin.
/// </summary>
string Version { get; }
/// <summary>
/// Gets the optional startup object of this plugin.
/// </summary>
IPluginStartup? Startup { get; }
/// <summary>
/// Gets the instance of this plugin.
/// </summary>
IPlugin? Instance { get; }
/// <summary>
/// Gets the instance type of this plugin.
/// </summary>
Type PluginType { get; }
}
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Impostor.Api.Plugins
{
public interface IPluginManager
{
IReadOnlyList<IPluginInformation> Plugins { get; }
}
}
@@ -9,7 +9,7 @@ namespace Impostor.Benchmarks.Data
{
public class MessageReader_Bytes_Pooled
{
private static readonly ConcurrentQueue<MessageReader_Bytes_Pooled> _readers;
private static ConcurrentQueue<MessageReader_Bytes_Pooled> _readers;
static MessageReader_Bytes_Pooled()
{
@@ -11,7 +11,7 @@ namespace Impostor.Benchmarks.Data
{
public MessageType SendOption { get; private set; }
private readonly Stack<int> _messageStarts = new Stack<int>();
private Stack<int> messageStarts = new Stack<int>();
public MessageWriter(byte[] buffer)
{
@@ -78,7 +78,7 @@ namespace Impostor.Benchmarks.Data
///
public void StartMessage(byte typeFlag)
{
_messageStarts.Push(this.Position);
messageStarts.Push(this.Position);
this.Position += 2; // Skip for size
this.Write(typeFlag);
}
@@ -86,7 +86,7 @@ namespace Impostor.Benchmarks.Data
///
public void EndMessage()
{
var lastMessageStart = _messageStarts.Pop();
var lastMessageStart = messageStarts.Pop();
var length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte
this.Buffer[lastMessageStart] = (byte)length;
this.Buffer[lastMessageStart + 1] = (byte)(length >> 8);
@@ -95,13 +95,13 @@ namespace Impostor.Benchmarks.Data
///
public void CancelMessage()
{
this.Position = this._messageStarts.Pop();
this.Position = this.messageStarts.Pop();
this.Length = this.Position;
}
public void Clear(MessageType sendOption)
{
this._messageStarts.Clear();
this.messageStarts.Clear();
this.SendOption = sendOption;
this.Buffer[0] = (byte)sendOption;
switch (sendOption)
@@ -293,7 +293,7 @@ namespace Impostor.Benchmarks.Data
this.Write(msg.Buffer, offset, msg.Length - offset);
}
public static unsafe bool IsLittleEndian()
public unsafe static bool IsLittleEndian()
{
byte b;
unsafe
@@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<AssemblyTitle>Impostor</AssemblyTitle>
<Product>Impostor</Product>
<Copyright>Copyright © AeonLucid 2020</Copyright>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Impostor.Cli</AssemblyName>
<TargetFramework>net5.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20478.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,88 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using Impostor.Patcher.Shared;
using Impostor.Patcher.Shared.Events;
namespace Impostor.Patcher.Cli
{
internal static class Program
{
private static readonly AmongUsModifier Modifier = new AmongUsModifier();
internal static Task<int> Main(string[] args)
{
var rootCommand = new RootCommand
{
new Option<string>(
"--address",
"IP Address of the server, will prompt if not specified"
),
new Option<string>(
"--name",
() => AmongUsModifier.DefaultRegionName,
"Name for server region"
),
};
rootCommand.Handler = CommandHandler.Create<string, string>((address, name) =>
{
Modifier.RegionName = name;
Modifier.Error += ModifierOnError;
Modifier.Saved += ModifierOnSaved;
Console.WriteLine("Welcome to Impostor");
if (Modifier.TryLoadRegionInfo(out var regionInfo))
{
Console.WriteLine($"Currently selected region: {regionInfo.Name} ({regionInfo.Ping}, {regionInfo.Servers.Count} server(s))");
}
if (address != null)
{
return Modifier.SaveIpAsync(address);
}
return PromptAsync();
});
return rootCommand.InvokeAsync(args);
}
private static void ModifierOnSaved(object sender, SavedEventArgs e)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("The IP Address was saved, please (re)start Among Us.");
Console.ResetColor();
}
private static void WriteError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ResetColor();
}
private static void ModifierOnError(object sender, ErrorEventArgs e)
{
WriteError(e.Message);
}
private static async Task PromptAsync()
{
Console.WriteLine("Please enter in the IP Address of the server you would like to use for Among Us");
Console.WriteLine("If you want to stop playing on the server, simply select another region");
while (true)
{
Console.Write("> ");
if (await Modifier.SaveIpAsync(Console.ReadLine()))
{
return;
}
}
}
}
}
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Gameloop.Vdf;
using Gameloop.Vdf.Linq;
using Impostor.Patcher.Shared.Events;
using Impostor.Patcher.Shared.Innersloth;
using ErrorEventArgs = Impostor.Patcher.Shared.Events.ErrorEventArgs;
namespace Impostor.Patcher.Shared
{
public class AmongUsModifier
{
private const uint AppId = 945360;
public const string DefaultRegionName = "Impostor";
public const ushort DefaultPort = 22023;
private readonly string _amongUsDir;
private readonly string _regionFile;
public string RegionName { get; set; } = DefaultRegionName;
public AmongUsModifier()
{
var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "LocalLow");
if (!Directory.Exists(appData))
{
appData = FindProtonAppData();
}
if (appData == null)
return;
_amongUsDir = Path.Combine(appData, "Innersloth", "Among Us");
_regionFile = Path.Combine(_amongUsDir, "regionInfo.dat");
}
private string FindProtonAppData()
{
string steamApps;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".steam", "steam", "steamapps");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Steam", "steamapps");
}
else
{
return null;
}
if (!Directory.Exists(steamApps))
return null;
var libraries = new List<string>
{
steamApps,
};
var vdf = Path.Combine(steamApps, "libraryfolders.vdf");
if (File.Exists(vdf))
{
var libraryFolders = VdfConvert.Deserialize(File.ReadAllText(vdf));
foreach (var libraryFolder in libraryFolders.Value.Children<VProperty>())
{
if (!int.TryParse(libraryFolder.Key, out _))
continue;
libraries.Add(Path.Combine(libraryFolder.Value.Value<string>(), "steamapps"));
}
}
foreach (var library in libraries)
{
var path = Path.Combine(library, "compatdata", AppId.ToString(), "pfx", "drive_c", "users", "steamuser", "AppData", "LocalLow");
if (Directory.Exists(path))
{
return path;
}
}
return null;
}
public async Task<bool> SaveIpAsync(string input)
{
// Filter out whitespace.
input = input.Trim();
// Split port from ip.
// Only IPv4 is supported so just do it simple.
var ip = string.Empty;
var port = DefaultPort;
var parts = input.Split(':');
if (parts.Length >= 1)
{
ip = parts[0];
}
if (parts.Length >= 2)
{
ushort.TryParse(parts[1], out port);
}
// Check if a valid IP address was entered.
if (!IPAddress.TryParse(ip, out var ipAddress))
{
// Attempt to resolve DNS.
try
{
var hostAddresses = await Dns.GetHostAddressesAsync(ip);
if (hostAddresses.Length == 0)
{
OnError("Invalid IP Address entered");
return false;
}
// Use first IPv4 result.
ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork);
}
catch (SocketException)
{
OnError("Failed to resolve hostname.");
return false;
}
}
// Only IPv4.
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
OnError("Invalid IP Address entered, only IPv4 is allowed.");
return false;
}
var result = WriteIp(ipAddress, port);
OnSaved(ip, port);
return result;
}
/// <summary>
/// Writes an IP Address to the Among Us region file.
/// </summary>
/// <param name="ipAddress">The IPv4 address to write.</param>
/// <param name="port"></param>
private bool WriteIp(IPAddress ipAddress, ushort port)
{
if (ipAddress == null ||
ipAddress.AddressFamily != AddressFamily.InterNetwork)
{
throw new ArgumentException(nameof(ipAddress));
}
if (!Directory.Exists(_amongUsDir))
{
OnError("Among Us directory was not found, is it installed? Try running it once.");
return false;
}
using (var file = File.Open(_regionFile, FileMode.Create, FileAccess.Write))
using (var writer = new BinaryWriter(file))
{
var ip = ipAddress.ToString();
var region = new RegionInfo(RegionName, ip, new[]
{
new ServerInfo($"{RegionName}-Master-1", ip, port),
});
region.Serialize(writer);
return true;
}
}
/// <summary>
/// Loads the existing region info from the Among Us.
/// </summary>
public bool TryLoadRegionInfo(out RegionInfo regionInfo)
{
regionInfo = null;
if (!File.Exists(_regionFile))
{
return false;
}
using (var file = File.Open(_regionFile, FileMode.Open, FileAccess.Read))
using (var reader = new BinaryReader(file))
{
try
{
regionInfo = RegionInfo.Deserialize(reader);
return true;
}
catch (Exception exception)
{
OnError("Couldn't parse region info\n" + exception);
return false;
}
}
}
/// <summary>
/// Loads the existing IP Address from the Among Us region file
/// if it was set by Impostor before.
/// </summary>
public bool TryLoadIp(out string ipAddress)
{
ipAddress = null;
if (!TryLoadRegionInfo(out var regionInfo))
{
return false;
}
if ((regionInfo.Name == RegionName || regionInfo.Name == DefaultRegionName) && regionInfo.Servers.Count >= 1)
{
ipAddress = regionInfo.Servers.ElementAt(0).Ip;
return true;
}
return false;
}
private void OnError(string message)
{
Error?.Invoke(this, new ErrorEventArgs(message));
}
private void OnSaved(string ipAddress, ushort port)
{
Saved?.Invoke(this, new SavedEventArgs(ipAddress, port));
}
public event EventHandler<ErrorEventArgs> Error;
public event EventHandler<SavedEventArgs> Saved;
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Impostor.Patcher.Shared
{
public class Configuration
{
private const string FileRecentIps = @"recent_ips.txt";
private const int MaxRecentIps = 5;
private readonly string _baseDir;
private readonly string _recentIpsPath;
private readonly List<string> _recentIps;
public Configuration()
{
var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
_baseDir = Path.Combine(appData, "Impostor");
_recentIpsPath = Path.Combine(_baseDir, FileRecentIps);
_recentIps = new List<string>();
}
public IReadOnlyList<string> RecentIps => _recentIps;
public void Load()
{
if (File.Exists(_recentIpsPath))
{
_recentIps.AddRange(File.ReadAllLines(_recentIpsPath));
}
}
public void Save()
{
Directory.CreateDirectory(_baseDir);
if (!Directory.Exists(_baseDir))
{
return;
}
if (_recentIps.Count > 0)
{
File.WriteAllLines(_recentIpsPath, _recentIps);
}
}
public void AddIp(string ip)
{
if (_recentIps.Contains(ip))
{
_recentIps.Remove(ip);
}
_recentIps.Insert(0, ip);
if (_recentIps.Count > MaxRecentIps)
{
_recentIps.RemoveAt(MaxRecentIps);
}
}
}
}
@@ -0,0 +1,14 @@
using System;
namespace Impostor.Patcher.Shared.Events
{
public class ErrorEventArgs : EventArgs
{
public ErrorEventArgs(string message)
{
Message = message;
}
public string Message { get; }
}
}
@@ -0,0 +1,16 @@
using System;
namespace Impostor.Patcher.Shared.Events
{
public class SavedEventArgs : EventArgs
{
public SavedEventArgs(string ipAddress, ushort port)
{
IpAddress = ipAddress;
Port = port;
}
public string IpAddress { get; }
public ushort Port { get; }
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Gameloop.Vdf" Version="0.6.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
namespace Impostor.Patcher.Shared.Innersloth
{
public class RegionInfo
{
public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers)
{
Name = name;
Ping = ping;
Servers = servers;
}
public string Name { get; }
public string Ping { get; }
public IReadOnlyList<ServerInfo> Servers { get; }
public void Serialize(BinaryWriter writer)
{
writer.Write(0);
writer.Write(Name);
writer.Write(Ping);
writer.Write(Servers.Count);
foreach (var server in Servers)
{
server.Serialize(writer);
}
}
public static RegionInfo Deserialize(BinaryReader reader)
{
var unknown = reader.ReadInt32();
var name = reader.ReadString();
var ping = reader.ReadString();
var servers = new List<ServerInfo>();
var serverCount = reader.ReadInt32();
for (var i = 0; i < serverCount; i++)
{
servers.Add(ServerInfo.Deserialize(reader));
}
return new RegionInfo(name, ping, servers);
}
}
}
@@ -0,0 +1,37 @@
using System.IO;
using System.Net;
namespace Impostor.Patcher.Shared.Innersloth
{
public class ServerInfo
{
public string Name { get; }
public string Ip { get; }
public ushort Port { get; }
public ServerInfo(string name, string ip, ushort port)
{
Name = name;
Ip = ip;
Port = port;
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Name);
writer.Write(IPAddress.Parse(Ip).GetAddressBytes());
writer.Write(Port);
writer.Write(0);
}
public static ServerInfo Deserialize(BinaryReader reader)
{
var name = reader.ReadString();
var ip = new IPAddress(reader.ReadBytes(4)).ToString();
var port = reader.ReadUInt16();
var unknown = reader.ReadInt32();
return new ServerInfo(name, ip, port);
}
}
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>
@@ -0,0 +1,141 @@
namespace Impostor.Patcher.WinForms.Forms
{
partial class FrmMain
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain));
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.buttonLaunch = new System.Windows.Forms.Button();
this.lblUrl = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.comboIp = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label1.Location = new System.Drawing.Point(28, 139);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(60, 13);
this.label1.TabIndex = 1;
this.label1.Text = "IP Address";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label2.Location = new System.Drawing.Point(28, 23);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(225, 91);
this.label2.TabIndex = 0;
this.label2.Text = "Welcome to Impostor\r\n\r\nPlease enter in the IP Address of the \r\nserver you would l" +
"ike to use for Among Us\r\n\r\nIf you want to stop playing on the server, \r\nsimply s" +
"elect another region";
//
// buttonLaunch
//
this.buttonLaunch.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonLaunch.Location = new System.Drawing.Point(179, 155);
this.buttonLaunch.Name = "buttonLaunch";
this.buttonLaunch.Size = new System.Drawing.Size(74, 22);
this.buttonLaunch.TabIndex = 3;
this.buttonLaunch.Text = "Save";
this.buttonLaunch.UseVisualStyleBackColor = true;
this.buttonLaunch.Click += new System.EventHandler(this.buttonLaunch_Click);
//
// lblUrl
//
this.lblUrl.AutoSize = true;
this.lblUrl.Cursor = System.Windows.Forms.Cursors.Hand;
this.lblUrl.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Underline, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.lblUrl.ForeColor = System.Drawing.SystemColors.Highlight;
this.lblUrl.Location = new System.Drawing.Point(39, 232);
this.lblUrl.Name = "lblUrl";
this.lblUrl.Size = new System.Drawing.Size(212, 13);
this.lblUrl.TabIndex = 5;
this.lblUrl.Text = "https://github.com/Impostor/Impostor";
this.lblUrl.Click += new System.EventHandler(this.lblUrl_Click);
//
// label3
//
this.label3.AutoSize = true;
this.label3.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label3.Location = new System.Drawing.Point(54, 216);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(182, 13);
this.label3.TabIndex = 6;
this.label3.Text = "Source code and latest versions at\r\n";
//
// comboIp
//
this.comboIp.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.comboIp.FormattingEnabled = true;
this.comboIp.Location = new System.Drawing.Point(31, 155);
this.comboIp.Name = "comboIp";
this.comboIp.Size = new System.Drawing.Size(141, 21);
this.comboIp.TabIndex = 2;
this.comboIp.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textIp_KeyDown);
//
// FrmMain
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(283, 262);
this.Controls.Add(this.comboIp);
this.Controls.Add(this.label3);
this.Controls.Add(this.lblUrl);
this.Controls.Add(this.buttonLaunch);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.ForeColor = System.Drawing.SystemColors.ControlText;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "FrmMain";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Impostor";
this.Load += new System.EventHandler(this.FrmMain_Load);
this.Shown += new System.EventHandler(this.FrmMain_Shown);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button buttonLaunch;
private System.Windows.Forms.Label lblUrl;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.ComboBox comboIp;
}
}
@@ -0,0 +1,114 @@
using System;
using System.Diagnostics;
using System.Windows.Forms;
using Impostor.Patcher.Shared;
using Impostor.Patcher.Shared.Events;
namespace Impostor.Patcher.WinForms.Forms
{
public partial class FrmMain : Form
{
private readonly Configuration _config;
private readonly AmongUsModifier _modifier;
public FrmMain()
{
InitializeComponent();
AcceptButton = buttonLaunch;
_config = new Configuration();
_modifier = new AmongUsModifier();
_modifier.Error += ModifierOnError;
_modifier.Saved += ModifierOnSaved;
}
private void ModifierOnError(object sender, ErrorEventArgs e)
{
MessageBox.Show(e.Message, "Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
comboIp.Text = string.Empty;
comboIp.Focus();
comboIp.Enabled = true;
buttonLaunch.Enabled = true;
}
private void ModifierOnSaved(object sender, SavedEventArgs e)
{
MessageBox.Show("The IP Address was saved, please (re)start Among Us.", "Success",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
var ipText = e.Port == AmongUsModifier.DefaultPort
? e.IpAddress
: $"{e.IpAddress}:{e.Port}";
comboIp.Text = ipText;
comboIp.Enabled = true;
buttonLaunch.Enabled = true;
_config.AddIp(ipText);
_config.Save();
RefreshComboIps();
}
private void FrmMain_Load(object sender, EventArgs e)
{
_config.Load();
RefreshComboIps();
if (_modifier.TryLoadIp(out var ipAddress))
{
comboIp.Text = ipAddress;
}
}
private void FrmMain_Shown(object sender, EventArgs e)
{
comboIp.Focus();
}
private void textIp_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode != Keys.Enter)
{
return;
}
e.Handled = true;
buttonLaunch_Click(this, EventArgs.Empty);
}
private async void buttonLaunch_Click(object sender, EventArgs e)
{
comboIp.Enabled = false;
buttonLaunch.Enabled = false;
await _modifier.SaveIpAsync(comboIp.Text);
}
private void lblUrl_Click(object sender, EventArgs e)
{
Process.Start("https://github.com/Impostor/Impostor");
}
private void RefreshComboIps()
{
comboIp.Items.Clear();
if (_config.RecentIps.Count > 0)
{
foreach (var ip in _config.RecentIps)
{
comboIp.Items.Add(ip);
}
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<AssemblyName>Impostor</AssemblyName>
<ProjectGuid>{804CF172-0C87-4423-9688-BD97D549891E}</ProjectGuid>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Copyright>Copyright © AeonLucid 2020</Copyright>
<ApplicationIcon>icon.ico</ApplicationIcon>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" />
<PackageReference Include="System.Resources.Extensions" Version="5.0.0" />
<Reference Include="System.Runtime.InteropServices.RuntimeInformation" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
using System;
using System.Windows.Forms;
using Impostor.Patcher.WinForms.Forms;
namespace Impostor.Patcher.WinForms
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FrmMain());
}
}
}
@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Impostor.Patcher.WinForms.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
"4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
"CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp =
new global::System.Resources.ResourceManager("Impostor.Client.WinForms.Properties.Resources",
typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get { return resourceCulture; }
set { resourceCulture = value; }
}
}
}
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Impostor.Patcher.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance =
((Settings) (global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get { return defaultInstance; }
}
}
}
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

@@ -1,3 +1,4 @@
using System.Linq;
using System.Threading.Tasks;
using Impostor.Api.Games.Managers;
using Impostor.Api.Innersloth;
@@ -11,17 +12,21 @@ namespace Impostor.Plugins.Example
{
private readonly ILogger<ExamplePlugin> _logger;
private readonly IGameManager _gameManager;
private readonly IPluginManager _pluginManager;
public ExamplePlugin(ILogger<ExamplePlugin> logger, IGameManager gameManager)
public ExamplePlugin(ILogger<ExamplePlugin> logger, IGameManager gameManager, IPluginManager pluginManager)
{
_logger = logger;
_gameManager = gameManager;
_pluginManager = pluginManager;
}
public override async ValueTask EnableAsync()
{
_logger.LogInformation("Example is being enabled.");
_logger.LogInformation("Following plugins are installed: {@plugins}", _pluginManager.Plugins.Select(x => $"{x.Id} ({x.Version})"));
var game = await _gameManager.CreateAsync(new GameOptionsData());
if (game == null)
{
@@ -4,7 +4,7 @@
{
public const string Error = "There was an internal server error. " +
"Check the server console for more information. " +
"Please report the issue on the Impostor GitHub if it keeps happening.";
"Please report the issue on the AmongUsServer GitHub if it keeps happening.";
public const string Destroyed = "The game you tried to join is being destroyed. " +
"Please create a new game.";
@@ -15,11 +15,5 @@
public const string UsernameLength = "Your username is too long, please make it shorter.";
public const string UsernameIllegalCharacters = "Your username contains illegal characters, please remove them.";
public const string VersionClientTooOld = "Please update your game to play on this server.";
public const string VersionServerTooOld = "Your client is too new, please update your Impostor server to play.";
public const string VersionUnsupported = "Your client version is unsupported, please update your Game and/or Impostor server.";
}
}
@@ -1,9 +1,11 @@
using System.Collections.Generic;
namespace Impostor.Server.Plugins
namespace Impostor.Server.Config
{
public class PluginConfig
public class PluginLoaderConfig
{
public const string Section = "PluginLoader";
public List<string> Paths { get; set; } = new List<string>();
public List<string> LibraryPaths { get; set; } = new List<string>();
@@ -19,6 +19,8 @@ namespace Impostor.Server.Events.Announcements
public class AnnouncementResponse : IAnnouncementRequestEvent.IResponse
{
public FreeWeekendState FreeWeekendState { get; set; } = FreeWeekendState.NotFree;
public bool UseCached { get; set; } = false;
public Announcement? Announcement { get; set; } = null;
+3 -1
View File
@@ -26,10 +26,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.4" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.7" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.333">
<PrivateAssets>all</PrivateAssets>
@@ -4,6 +4,7 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Events.Managers;
using Impostor.Api.Innersloth;
using Impostor.Api.Net.Messages;
using Impostor.Api.Net.Messages.Announcements;
using Impostor.Hazel;
@@ -100,6 +101,15 @@ namespace Impostor.Server.Net
_logger.LogDebug("Sent ({Id}) {Message}", announcement.Id, announcement.Message);
}
if (response.FreeWeekendState != FreeWeekendState.NotFree)
{
using var writer = MessageWriter.Get(MessageType.Reliable);
Message02SetFreeWeekend.Serialize(writer, response.FreeWeekendState);
await e.Connection.SendAsync(writer);
_logger.LogDebug("Sent {FreeWeekendState} weekend state", response.FreeWeekendState);
}
await e.Connection.Disconnect(null);
}
}
@@ -26,7 +26,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
private readonly ObjectPool<PlayerMovementEvent> _pool;
private ushort _lastSequenceId;
private AirshipSpawnState _spawnState;
private bool _spawnSnapAllowed;
public InnerCustomNetworkTransform(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerCustomNetworkTransform> logger, InnerPlayerControl playerControl, IEventManager eventManager, ObjectPool<PlayerMovementEvent> pool) : base(customMessageManager, game)
{
@@ -36,13 +36,6 @@ namespace Impostor.Server.Net.Inner.Objects.Components
_pool = pool;
}
private enum AirshipSpawnState : byte
{
PreSpawn,
SelectingSpawn,
Spawned,
}
public Vector2 Position { get; private set; }
public Vector2 Velocity { get; private set; }
@@ -105,17 +98,15 @@ namespace Impostor.Server.Net.Inner.Objects.Components
if (Game.GameNet.ShipStatus is InnerAirshipStatus airshipStatus)
{
// As part of airship spawning, clients are sending snap to -25 40 to move themself out of view
if (_spawnState == AirshipSpawnState.PreSpawn && Approximately(position, airshipStatus.PreSpawnLocation))
// As part of airship spawning, clients are sending snap to -25 40 for no reason(?), cancelling it works just fine
if (Approximately(position, airshipStatus.PreSpawnLocation))
{
_spawnState = AirshipSpawnState.SelectingSpawn;
return true;
return false;
}
// Once the spawn has been selected, the client sends a second snap to the select spawn location
if (_spawnState == AirshipSpawnState.SelectingSpawn && airshipStatus.SpawnLocations.Any(location => Approximately(position, location)))
if (_spawnSnapAllowed && airshipStatus.SpawnLocations.Any(location => Approximately(position, location)))
{
_spawnState = AirshipSpawnState.Spawned;
_spawnSnapAllowed = false;
return true;
}
}
@@ -161,7 +152,7 @@ namespace Impostor.Server.Net.Inner.Objects.Components
internal void OnPlayerSpawn()
{
_spawnState = AirshipSpawnState.PreSpawn;
_spawnSnapAllowed = true;
}
private static bool SidGreaterThan(ushort newSid, ushort prevSid)
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Impostor.Api.Events.Managers;
using Impostor.Api.Innersloth;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Inner;
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Impostor.Api;
using Impostor.Api.Events.Managers;
using Impostor.Api.Innersloth.Maps;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Inner;
@@ -9,27 +9,32 @@ namespace Impostor.Server.Net.Inner.Objects
{
public class PlayerVoteArea : IInnerMeetingHud.IPlayerVoteArea
{
private byte _votedForId;
private const byte VoteMask = 15;
private const byte ReportedBit = 32;
private const byte VotedBit = 64;
private const byte DeadBit = 128;
private sbyte _votedForId;
public PlayerVoteArea(InnerMeetingHud parent, InnerPlayerInfo targetPlayer, bool isDead)
{
Parent = parent;
TargetPlayer = targetPlayer;
VotedForId = (byte)(isDead ? TVoteType.Dead : TVoteType.HasNotVoted);
IsDead = isDead;
}
public InnerMeetingHud Parent { get; }
public InnerPlayerInfo TargetPlayer { get; }
public bool IsDead => VoteType == TVoteType.Dead;
public bool IsDead { get; private set; }
public bool DidVote => VoteType != TVoteType.HasNotVoted;
public bool DidVote { get; private set; }
public bool DidReport { get; private set; }
public byte VotedForId
public sbyte VotedForId
{
get => _votedForId;
@@ -37,22 +42,25 @@ namespace Impostor.Server.Net.Inner.Objects
{
_votedForId = value;
switch ((VoteType)value)
if (DidVote)
{
case TVoteType.Dead:
VoteType = TVoteType.Dead;
break;
switch ((VoteType)value)
{
case TVoteType.ForceSkip:
case TVoteType.Skip:
VoteType = (VoteType)value;
break;
case TVoteType.HasNotVoted:
case TVoteType.Missed:
case TVoteType.Skipped:
VoteType = (VoteType)value;
break;
default:
VoteType = TVoteType.Player;
VotedFor = Parent.Game.GameNet.GameData!.GetPlayerById(value)?.Controller;
break;
default:
VoteType = TVoteType.Player;
VotedFor = Parent.Game.GameNet.GameData!.GetPlayerById((byte)value)?.Controller;
break;
}
}
else
{
VoteType = null;
VotedFor = null;
}
}
}
@@ -65,20 +73,39 @@ namespace Impostor.Server.Net.Inner.Objects
internal void Deserialize(IMessageReader reader, bool updateVote)
{
var votedForId = reader.ReadByte();
var state = reader.ReadByte();
DeserializeState(state, out var votedForId, out var isDead, out var didVote, out var didReport);
IsDead = isDead;
DidReport = didReport;
if (updateVote)
{
DidVote = didVote;
VotedForId = votedForId;
}
DidReport = reader.ReadBoolean();
}
internal void SetVotedFor(byte votedFor)
internal void SetVotedFor(sbyte votedFor)
{
DidVote = true;
VotedForId = votedFor;
}
private static void DeserializeState(byte state, out sbyte votedForId, out bool isDead, out bool didVote, out bool didReport)
{
votedForId = (sbyte)((state & VoteMask) - 1);
// Among Us's meeting system is really scuffed, we need this to ensure that ForceSkip state is correct (this will break any 14+ players game)
if (votedForId == ((sbyte)TVoteType.ForceSkip + 1 & 14))
{
votedForId = (sbyte)TVoteType.ForceSkip;
}
isDead = (state & DeadBit) > 0;
didVote = (state & VotedBit) > 0;
didReport = (state & ReportedBit) > 0;
}
}
}
}
@@ -1,32 +0,0 @@
using Impostor.Api.Net.Messages;
namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerMeetingHud
{
public class VoterState
{
public VoterState(byte voterId, byte votedForId)
{
VoterId = voterId;
VotedForId = votedForId;
}
public byte VoterId { get; }
public byte VotedForId { get; }
public static VoterState Deserialize(IMessageReader reader)
{
return new VoterState(reader.Tag, reader.ReadByte());
}
public void Serialize(IMessageWriter writer)
{
writer.StartMessage(this.VoterId);
writer.Write(this.VotedForId);
writer.EndMessage();
}
}
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
@@ -43,8 +42,7 @@ namespace Impostor.Server.Net.Inner.Objects
{
try
{
const float AnimationTime = 0.25f + 0.5f + 0.4f + 3f + 0.75f + 5f;
await Task.Delay(TimeSpan.FromSeconds(AnimationTime + Game.Options.DiscussionTime + Game.Options.VotingTime), _timerToken.Token);
await Task.Delay(TimeSpan.FromSeconds(Game.Options.DiscussionTime + Game.Options.VotingTime), _timerToken.Token);
}
catch (TaskCanceledException)
{
@@ -72,31 +70,37 @@ namespace Impostor.Server.Net.Inner.Objects
if (initialState)
{
PopulateButtons();
}
var length = reader.ReadPackedUInt32();
for (var i = 0; i < length; i++)
{
var inner = reader.ReadMessage();
var playerVoteArea = _playerStates.SingleOrDefault(x => x.TargetPlayer.PlayerId == inner.Tag);
if (playerVoteArea != null)
foreach (var playerState in _playerStates)
{
var clientPlayer = Game.Players.SingleOrDefault(x => x.Character?.PlayerId == playerVoteArea.TargetPlayer.PlayerId);
var updateVote = !playerVoteArea.DidVote && (clientPlayer?.IsHost ?? false) && playerVoteArea.VoteType != VoteType.Missed;
playerState.Deserialize(reader, false);
playerVoteArea.Deserialize(inner, updateVote);
if (updateVote)
if (playerState.DidReport)
{
await HandleVoteAsync(playerVoteArea);
await CheckForEndVotingAsync();
Reporter = playerState.TargetPlayer;
}
}
}
else
{
var num = reader.ReadPackedUInt32();
if (initialState && playerVoteArea.DidReport)
for (var i = 0; i < _playerStates.Length; i++)
{
if ((num & 1 << i) != 0)
{
Reporter = playerVoteArea.TargetPlayer;
var playerVoteArea = _playerStates[i];
var clientPlayer = Game.Players.SingleOrDefault(x => x.Character?.PlayerId == playerVoteArea.TargetPlayer.PlayerId);
var isHost = (clientPlayer?.IsHost ?? false) && playerVoteArea.VoteType != VoteType.ForceSkip;
playerVoteArea.Deserialize(reader, isHost);
if (isHost)
{
await HandleVoteAsync(playerVoteArea);
await CheckForEndVotingAsync();
}
}
}
}
@@ -173,7 +177,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
private async ValueTask<bool> HandleCastVoteAsync(ClientPlayer sender, ClientPlayer? target, byte playerId, byte suspectPlayerId)
private async ValueTask<bool> HandleCastVoteAsync(ClientPlayer sender, ClientPlayer? target, byte playerId, sbyte suspectPlayerId)
{
if (sender.IsHost)
{
@@ -217,47 +221,48 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
private KeyValuePair<byte, int> MaxPair(Dictionary<byte, int> self, out bool tie)
private byte[] CalculateVotes()
{
tie = true;
var result = new KeyValuePair<byte, int>(byte.MaxValue, int.MinValue);
foreach (var keyValuePair in self)
byte[] array = new byte[_playerStates.Max(x => x.TargetPlayer.PlayerId) + 2];
foreach (var playerVoteArea in _playerStates)
{
if (keyValuePair.Value > result.Value)
if (playerVoteArea.DidVote)
{
result = keyValuePair;
var index = playerVoteArea.VotedForId + 1;
if (index >= 0 && index < array.Length)
{
array[index] += 1;
}
}
}
return array;
}
private int IndexOfMax<T>(T[] self, Func<T, int> comparer, out bool tie)
{
tie = false;
var num = int.MinValue;
var result = -1;
for (var i = 0; i < self.Length; i++)
{
var num2 = comparer.Invoke(self[i]);
if (num2 > num)
{
result = i;
num = num2;
tie = false;
}
else if (keyValuePair.Value == result.Value)
else if (num2 == num)
{
tie = true;
result = -1;
}
}
return result;
}
private Dictionary<byte, int> CalculateVotes()
{
var players = new Dictionary<byte, int>();
foreach (var playerVoteArea in _playerStates)
{
if (!playerVoteArea.IsDead && playerVoteArea.DidVote && playerVoteArea.VoteType != VoteType.Missed)
{
if (players.TryGetValue(playerVoteArea.VotedForId, out var current))
{
players[playerVoteArea.VotedForId] = current + 1;
}
else
{
players[playerVoteArea.VotedForId] = 1;
}
}
}
return players;
}
private async ValueTask HandleVotingCompleteAsync()
{
_timerToken.Cancel();
@@ -266,14 +271,14 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (!playerVoteArea.DidVote)
{
playerVoteArea.SetVotedFor((byte)VoteType.Missed);
playerVoteArea.SetVotedFor((sbyte)VoteType.ForceSkip);
await HandleVoteAsync(playerVoteArea);
}
}
var self = this.CalculateVotes();
var max = MaxPair(self, out var tie);
var exiled = tie ? null : Game.GameNet.GameData!.GetPlayerById(max.Key)?.Controller;
byte[] self = this.CalculateVotes();
var maxIdx = IndexOfMax(self, p => p, out var tie) - 1;
var exiled = Game.GameNet.GameData!.GetPlayerById((byte)maxIdx)?.Controller;
if (exiled != null)
{
@@ -297,17 +297,6 @@ namespace Impostor.Server.Net.Inner.Objects
break;
}
case RpcCalls.SendQuickChat:
{
if (!await ValidateOwnership(call, sender))
{
return false;
}
// TODO: deserialize and expose the result in an API
break;
}
default:
return await base.HandleRpcAsync(sender, target, call, reader);
}
@@ -427,7 +416,7 @@ namespace Impostor.Server.Net.Inner.Objects
var i = 1;
while (true)
{
var text = expected + " " + i;
string text = expected + " " + i;
if (Game.Players.All(x => x.Character == null || x.Character == this || x.Character.PlayerInfo.PlayerName != text))
{
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using Impostor.Api.Events.Managers;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.Customization;
using Impostor.Api.Net.Messages;
using Impostor.Api.Utils;
using Impostor.Server.Net.State;
namespace Impostor.Server.Net.Inner.Objects
{
@@ -24,9 +24,9 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
public override Vector2 InitialSpawnCenter { get; } = new Vector2(16.64f, -2.46f);
public override Vector2 MeetingSpawnCenter { get; } = new Vector2(17.4f, -16.286f);
public override Vector2 MeetingSpawnCenter { get; } = new Vector2(17.726f, -16.286f);
public Vector2 MeetingSpawnCenter2 { get; } = new Vector2(17.4f, -17.515f);
public Vector2 MeetingSpawnCenter2 { get; } = new Vector2(-17.7f, -17.5f);
public override Vector2 GetSpawnLocation(InnerPlayerControl player, int numPlayers, bool initialSpawn)
{
@@ -35,16 +35,17 @@ namespace Impostor.Server.Net.Inner.Objects.ShipStatus
return base.GetSpawnLocation(player, numPlayers, initialSpawn);
}
var halfPlayers = numPlayers / 2; // floored intentionally
var spawnId = player.PlayerId % 15;
if (player.PlayerId < halfPlayers)
Vector2 position;
if (player.PlayerId < 5)
{
return this.MeetingSpawnCenter + (new Vector2(0.6f, 0) * spawnId);
position = this.MeetingSpawnCenter + (new Vector2(1, 0) * player.PlayerId);
}
else
{
return this.MeetingSpawnCenter2 + (new Vector2(0.6f, 0) * (spawnId - halfPlayers));
position = this.MeetingSpawnCenter2 + (new Vector2(1, 0) * (player.PlayerId - 5));
}
return position;
}
protected override void AddSystems(Dictionary<SystemTypes, ISystemType> systems)
@@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -19,10 +18,10 @@ namespace Impostor.Server.Net.Manager
{
internal partial class ClientManager
{
// NOTE: when updating this array, keep the versions ordered from old to new, otherwise the version compare logic doesn't work properly
private static readonly int[] SupportedVersions =
private static readonly HashSet<int> SupportedVersions = new HashSet<int>
{
GameVersion.GetVersion(2021, 4, 25), // 2021.6.15
GameVersion.GetVersion(2021, 3, 25), // 2021.3.31s
GameVersion.GetVersion(2021, 4, 2), // 2021.4.2a
};
private readonly ILogger<ClientManager> _logger;
@@ -39,14 +38,6 @@ namespace Impostor.Server.Net.Manager
_clients = new ConcurrentDictionary<int, ClientBase>();
}
private enum VersionCompareResult
{
Compatible,
ClientTooOld,
ServerTooOld,
Unknown,
}
public IEnumerable<ClientBase> Clients => _clients.Values;
public int NextId()
@@ -67,24 +58,10 @@ namespace Impostor.Server.Net.Manager
public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion)
{
var versionCompare = CompareVersion(clientVersion);
if (versionCompare != VersionCompareResult.Compatible)
if (!SupportedVersions.Contains(clientVersion))
{
GameVersion.ParseVersion(clientVersion, out var year, out var month, out var day, out var revision);
_logger.LogTrace("Client connected using unsupported version: {clientVersion} ({version})", clientVersion, $"{year}.{month}.{day}{(revision == 0 ? string.Empty : "." + revision)}");
using var packet = MessageWriter.Get(MessageType.Reliable);
var message = versionCompare switch
{
VersionCompareResult.ClientTooOld => DisconnectMessages.VersionClientTooOld,
VersionCompareResult.ServerTooOld => DisconnectMessages.VersionServerTooOld,
VersionCompareResult.Unknown => DisconnectMessages.VersionUnsupported,
_ => throw new ArgumentOutOfRangeException(),
};
Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, message);
Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
await connection.SendAsync(packet);
return;
}
@@ -127,29 +104,5 @@ namespace Impostor.Server.Net.Manager
&& _clients.TryGetValue(client.Id, out var registeredClient)
&& ReferenceEquals(client, registeredClient);
}
private VersionCompareResult CompareVersion(int clientVersion)
{
foreach (var serverVersion in SupportedVersions)
{
if (clientVersion == serverVersion)
{
return VersionCompareResult.Compatible;
}
}
if (clientVersion < SupportedVersions[0])
{
return VersionCompareResult.ClientTooOld;
}
if (clientVersion > SupportedVersions.Last())
{
return VersionCompareResult.ServerTooOld;
}
// This may happen in the very rare case that version X is supported, X+2 is as well, but X+1 is not.
return VersionCompareResult.Unknown;
}
}
}
@@ -8,17 +8,20 @@ namespace Impostor.Server.Plugins
{
private Assembly? _assembly;
public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin)
public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin, bool isDefaultAssembly)
{
AssemblyName = assemblyName;
Path = path;
IsPlugin = isPlugin;
IsDefaultAssembly = isDefaultAssembly;
}
public string Path { get; }
public bool IsPlugin { get; }
public bool IsDefaultAssembly { get; }
public AssemblyName AssemblyName { get; }
public Assembly Load(AssemblyLoadContext context)
@@ -9,6 +9,8 @@ namespace Impostor.Server.Plugins
bool IsPlugin { get; }
bool IsDefaultAssembly { get; }
Assembly Load(AssemblyLoadContext context);
}
}
@@ -7,16 +7,19 @@ namespace Impostor.Server.Plugins
{
private readonly Assembly _assembly;
public LoadedAssemblyInformation(Assembly assembly)
public LoadedAssemblyInformation(Assembly assembly, bool isDefaultAssembly)
{
AssemblyName = assembly.GetName();
_assembly = assembly;
IsDefaultAssembly = isDefaultAssembly;
}
public AssemblyName AssemblyName { get; }
public bool IsPlugin => false;
public bool IsDefaultAssembly { get; }
public Assembly Load(AssemblyLoadContext context)
{
return _assembly;
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Loader;
namespace Impostor.Server.Plugins
{
public class PluginAssemblyLoadContext : AssemblyLoadContext, IDisposable
{
private readonly IDictionary<string, IAssemblyInformation> _assembliesByName;
public PluginAssemblyLoadContext(IDictionary<string, IAssemblyInformation> assembliesByName)
: base(true)
{
_assembliesByName = assembliesByName;
}
public void Dispose()
{
_assembliesByName.Clear();
this.Unload();
}
protected override Assembly? Load(AssemblyName name)
{
return name.Name != null && _assembliesByName.TryGetValue(name.Name, out var assemblyInfo)
? assemblyInfo.Load(this)
: null;
}
}
}
@@ -6,7 +6,7 @@ using Impostor.Api.Plugins;
namespace Impostor.Server.Plugins
{
public class PluginInformation
public class PluginInformation : IPluginInformation
{
private readonly ImpostorPluginAttribute _attribute;
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Plugins;
@@ -13,20 +12,20 @@ namespace Impostor.Server.Plugins
{
private readonly ILogger<PluginLoaderService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly List<PluginInformation> _plugins;
private readonly PluginManager _pluginManager;
public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, List<PluginInformation> plugins)
public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, PluginManager pluginManager)
{
_logger = logger;
_serviceProvider = serviceProvider;
_plugins = plugins;
_pluginManager = pluginManager;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Loading plugins.");
_logger.LogInformation("Enabling plugins.");
foreach (var plugin in _plugins)
foreach (var plugin in _pluginManager.Plugins)
{
_logger.LogInformation("Enabling plugin {0}.", plugin);
@@ -38,10 +37,10 @@ namespace Impostor.Server.Plugins
}
_logger.LogInformation(
_plugins.Count == 1
? "Loaded {0} plugin."
: "Loaded {0} plugins.",
_plugins.Count
_pluginManager.Plugins.Count == 1
? "Enabled {0} plugin."
: "Enabled {0} plugins.",
_pluginManager.Plugins.Count
);
}
@@ -49,7 +48,7 @@ namespace Impostor.Server.Plugins
{
// Disable all plugins with a valid instance set.
// In the case of a failed startup, some can be null.
foreach (var plugin in _plugins)
foreach (var plugin in _pluginManager.Plugins)
{
if (plugin.Instance != null)
{
@@ -57,6 +56,7 @@ namespace Impostor.Server.Plugins
// Disable plugin.
await plugin.Instance.DisableAsync();
plugin.Instance = null;
}
}
}
@@ -4,24 +4,72 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using Impostor.Api.Plugins;
using Impostor.Server.Config;
using Impostor.Server.Plugins.Proxies;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Hosting;
using Serilog;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Plugins
{
public static class PluginLoader
public class PluginManager : IPluginManager, IHostedService
{
private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader));
private readonly ILogger<PluginManager> _logger;
private readonly IOptions<PluginLoaderConfig> _pluginConfig;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;
private readonly ServiceProxyCollection _serviceProxyCollection;
public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config)
private PluginAssemblyLoadContext? _currentContext;
public PluginManager(ILogger<PluginManager> logger, IOptions<PluginLoaderConfig> pluginConfig, IConfiguration configuration, IServiceProvider serviceProvider, ServiceProxyCollection serviceProxyCollection)
{
var assemblyInfos = new List<IAssemblyInformation>();
var context = AssemblyLoadContext.Default;
_logger = logger;
_pluginConfig = pluginConfig;
_configuration = configuration;
_serviceProvider = serviceProvider;
_serviceProxyCollection = serviceProxyCollection;
}
public IHost? CurrentHost { get; private set; }
public List<PluginInformation> Plugins { get; } = new List<PluginInformation>();
IReadOnlyList<IPluginInformation> IPluginManager.Plugins => Plugins.AsReadOnly();
public Task StartAsync(CancellationToken cancellationToken)
{
CurrentHost = CreatePluginHost();
return CurrentHost.StartAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (CurrentHost != null)
{
await CurrentHost.StopAsync(cancellationToken);
if (CurrentHost is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
CurrentHost.Dispose();
}
}
}
private IDictionary<string, IAssemblyInformation> GetAssemblyInformation()
{
// Add the plugins and libraries.
var config = _pluginConfig.Value;
var pluginPaths = new List<string>(config.Paths);
var libraryPaths = new List<string>(config.LibraryPaths);
CheckPaths(pluginPaths);
@@ -35,38 +83,70 @@ namespace Impostor.Server.Plugins
var matcher = new Matcher(StringComparison.OrdinalIgnoreCase);
matcher.AddInclude("*.dll");
matcher.AddExclude("Impostor.Api.dll");
matcher.AddExclude("Impostor.Server.dll");
RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true);
RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false);
var assembliesByName = AssemblyLoadContext.Default.Assemblies
.Where(a => a.GetName().Name != null)
.ToDictionary(
a => a.GetName().Name!,
a => (IAssemblyInformation)new LoadedAssemblyInformation(a, true));
// Register the resolver to the current context.
// TODO: Move this to a new context so we can unload/reload plugins.
context.Resolving += (loadContext, name) =>
RegisterAssemblies(assembliesByName, pluginPaths, matcher, true);
RegisterAssemblies(assembliesByName, libraryPaths, matcher, false);
return assembliesByName;
}
private IHost CreatePluginHost()
{
var assembliesByName = GetAssemblyInformation();
_currentContext ??= new PluginAssemblyLoadContext(assembliesByName);
var pluginHostBuilder = new HostBuilder()
.UseConsoleLifetime(options => options.SuppressStatusMessages = true)
.UseContentRoot(_configuration.GetValue<string>(HostDefaults.ContentRootKey))
.ConfigureHostConfiguration(builder => { builder.AddConfiguration(_configuration); })
.UseServiceProviderFactory(_ => new DefaultServiceProviderFactory());
Plugins.Clear();
Plugins.AddRange(GetPluginInformation(assembliesByName, _currentContext));
foreach (var plugin in Plugins)
{
Logger.Verbose("Loading assembly {0} v{1}", name.Name, name.Version);
plugin.Startup?.ConfigureHost(pluginHostBuilder);
}
// Some plugins may be referencing another Impostor.Api version and try to load it.
// We want to only use the one shipped with the server.
if (name.Name == "Impostor.Api")
pluginHostBuilder.ConfigureServices(services =>
{
services.AddHostedService<PluginLoaderService>();
foreach (var (type, lifetime) in _serviceProxyCollection.ServiceTypes)
{
return typeof(IPlugin).Assembly;
services.Add(new ServiceDescriptor(
type,
_ => _serviceProvider.GetService(type)!,
lifetime));
}
var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name);
foreach (var plugin in Plugins)
{
plugin.Startup?.ConfigureServices(services);
}
});
return info?.Load(loadContext);
};
return pluginHostBuilder.Build();
}
// TODO: Catch uncaught exceptions.
var assemblies = assemblyInfos
private List<PluginInformation> GetPluginInformation(IDictionary<string, IAssemblyInformation> assembliesByName, PluginAssemblyLoadContext context)
{
var plugins = new List<PluginInformation>();
var pluginAssemblies = assembliesByName
.Values
.Where(a => a.IsPlugin)
.Select(a => context.LoadFromAssemblyName(a.AssemblyName))
.ToList();
// Find all plugins.
var plugins = new List<PluginInformation>();
foreach (var assembly in assemblies)
foreach (var assembly in pluginAssemblies)
{
// Find plugin startup.
var pluginStartup = assembly
@@ -76,7 +156,7 @@ namespace Impostor.Server.Plugins
if (pluginStartup.Count > 1)
{
Logger.Warning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly);
_logger.LogWarning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly);
continue;
}
@@ -91,7 +171,7 @@ namespace Impostor.Server.Plugins
if (plugin.Count != 1)
{
Logger.Warning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly);
_logger.LogWarning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly);
continue;
}
@@ -104,61 +184,75 @@ namespace Impostor.Server.Plugins
plugin.Single()));
}
var orderedPlugins = LoadOrderPlugins(plugins);
foreach (var plugin in orderedPlugins)
{
plugin.Startup?.ConfigureHost(builder);
}
builder.ConfigureServices(services =>
{
services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, orderedPlugins));
foreach (var plugin in orderedPlugins)
{
plugin.Startup?.ConfigureServices(services);
}
});
return builder;
return OrderPlugins(plugins);
}
private static void CheckPaths(IEnumerable<string> paths)
private void CheckPaths(IEnumerable<string> paths)
{
foreach (var path in paths)
{
if (!Directory.Exists(path))
{
Logger.Warning("Path {path} was specified in the PluginLoader configuration, but this folder doesn't exist!", path);
_logger.LogWarning("Path {path} was specified in the PluginLoader configuration, but this folder doesn't exist!", path);
}
}
}
private static void RegisterAssemblies(
private void RegisterAssemblies(
IDictionary<string, IAssemblyInformation> assemblies,
IEnumerable<string> paths,
Matcher matcher,
ICollection<IAssemblyInformation> assemblyInfos,
bool isPlugin)
{
foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath))
{
AssemblyName assemblyName;
AssemblyName name;
try
{
assemblyName = AssemblyName.GetAssemblyName(path);
name = AssemblyName.GetAssemblyName(path);
}
catch (BadImageFormatException)
{
continue;
}
assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin));
if (name.Name == null)
{
_logger.LogWarning(
"Skipped {0} because the assembly name could not be resolved",
path);
continue;
}
if (assemblies.TryGetValue(name.Name, out var loaded))
{
var version = name.Version;
var loadedVersion = loaded.AssemblyName.Version;
if (loadedVersion != null && version != null && version.Major != loadedVersion.Major)
{
_logger.LogError(
"Assembly {0} is already loaded with version {1} while loading version {2}",
name.Name,
loadedVersion,
version);
}
else
{
_logger.LogDebug("Skipped assembly {0} because it's already loaded", name.Name);
}
continue;
}
_logger.LogTrace("Registered assembly {name} v{version} ({path})", name.Name, name.Version, path);
assemblies.Add(name.Name, new AssemblyInformation(name, path, isPlugin, false));
}
}
private static List<PluginInformation> LoadOrderPlugins(IEnumerable<PluginInformation> plugins)
private List<PluginInformation> OrderPlugins(IEnumerable<PluginInformation> plugins)
{
var pluginDictionary = new Dictionary<string, PluginInformation>();
var hardDependencies = new Dictionary<string, List<string>>();
@@ -208,7 +302,7 @@ namespace Impostor.Server.Plugins
return ordered;
}
private static List<string> CheckHardDependencies(
private List<string> CheckHardDependencies(
List<string> plugins,
IReadOnlyDictionary<string, List<string>> hardDependencies)
{
@@ -221,7 +315,7 @@ namespace Impostor.Server.Plugins
foreach (var dependency in hardDependencies[plugin].Where(dependency => !plugins.Contains(dependency)))
{
Logger.Error(
_logger.LogError(
"The plugin {plugin} has defined the plugin {dependency} as a hard dependency but its not present! {plugin} will not loaded.",
plugin,
dependency,
@@ -239,7 +333,7 @@ namespace Impostor.Server.Plugins
return plugins;
}
private static void RecursiveOrder(
private void RecursiveOrder(
string plugin,
IReadOnlyDictionary<string, List<string>> dependencyGraph,
ICollection<string> processed,
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Impostor.Server.Plugins.Proxies
{
public class ServiceProxyCollection
{
public ServiceProxyCollection(IReadOnlyList<ServiceRegistration> serviceTypes)
{
ServiceTypes = serviceTypes;
}
public IReadOnlyList<ServiceRegistration> ServiceTypes { get; }
}
}
@@ -0,0 +1,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Impostor.Server.Plugins.Proxies
{
public record ServiceRegistration(Type Type, ServiceLifetime Lifetime);
}
+59 -46
View File
@@ -1,12 +1,15 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Impostor.Api.Events;
using Impostor.Api.Events.Managers;
using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Manager;
using Impostor.Api.Net.Messages;
using Impostor.Api.Plugins;
using Impostor.Api.Utils;
using Impostor.Hazel.Extensions;
using Impostor.Server.Config;
@@ -18,11 +21,13 @@ using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Messages;
using Impostor.Server.Net.Redirector;
using Impostor.Server.Plugins;
using Impostor.Server.Plugins.Proxies;
using Impostor.Server.Recorder;
using Impostor.Server.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Serilog;
using Serilog.Events;
@@ -31,38 +36,16 @@ namespace Impostor.Server
{
internal static class Program
{
private static int Main(string[] args)
private static async Task<int> Main(string[] args)
{
#if DEBUG
var logLevel = LogEventLevel.Debug;
#else
var logLevel = LogEventLevel.Information;
#endif
if (args.Contains("--verbose"))
{
logLevel = LogEventLevel.Verbose;
}
else if (args.Contains("--errors-only"))
{
logLevel = LogEventLevel.Error;
}
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(logLevel)
#if DEBUG
.MinimumLevel.Override("Microsoft", LogEventLevel.Debug)
#else
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
#endif
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
.CreateBootstrapLogger();
try
{
Log.Information("Starting Impostor v{0}", DotnetUtils.Version);
CreateHostBuilder(args).Build().Run();
await CreateHostBuilder(args).RunConsoleAsync();
return 0;
}
catch (Exception ex)
@@ -76,25 +59,8 @@ namespace Impostor.Server
}
}
private static IConfiguration CreateConfiguration(string[] args)
{
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.SetBasePath(Directory.GetCurrentDirectory());
configurationBuilder.AddJsonFile("config.json", true);
configurationBuilder.AddJsonFile("config.Development.json", true);
configurationBuilder.AddEnvironmentVariables(prefix: "IMPOSTOR_");
configurationBuilder.AddCommandLine(args);
return configurationBuilder.Build();
}
private static IHostBuilder CreateHostBuilder(string[] args)
{
var configuration = CreateConfiguration(args);
var pluginConfig = configuration.GetSection("PluginLoader")
.Get<PluginConfig>() ?? new PluginConfig();
return Host.CreateDefaultBuilder(args)
.UseContentRoot(Directory.GetCurrentDirectory())
#if DEBUG
@@ -104,7 +70,10 @@ namespace Impostor.Server
#endif
.ConfigureAppConfiguration(builder =>
{
builder.AddConfiguration(configuration);
builder.AddJsonFile("config.json", true);
builder.AddJsonFile("config.Development.json", true);
builder.AddEnvironmentVariables(prefix: "IMPOSTOR_");
builder.AddCommandLine(args);
})
.ConfigureServices((host, services) =>
{
@@ -134,6 +103,7 @@ namespace Impostor.Server
services.Configure<AnnouncementsServerConfig>(host.Configuration.GetSection(AnnouncementsServerConfig.Section));
services.Configure<AuthServerConfig>(host.Configuration.GetSection(AuthServerConfig.Section));
services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
services.Configure<PluginLoaderConfig>(host.Configuration.GetSection(PluginLoaderConfig.Section));
if (redirector.Enabled)
{
@@ -228,10 +198,53 @@ namespace Impostor.Server
{
services.AddHostedService<AuthService>();
}
services.AddSingleton<PluginManager>();
services.AddSingleton<IPluginManager>(p => p.GetRequiredService<PluginManager>());
services.AddHostedService(p => p.GetRequiredService<PluginManager>());
})
.UseSerilog()
.UseConsoleLifetime()
.UsePluginLoader(pluginConfig);
.UseSerilog((context, loggerConfiguration) =>
{
#if DEBUG
var logLevel = LogEventLevel.Debug;
#else
var logLevel = LogEventLevel.Information;
#endif
if (args.Contains("--verbose"))
{
logLevel = LogEventLevel.Verbose;
}
else if (args.Contains("--errors-only"))
{
logLevel = LogEventLevel.Error;
}
loggerConfiguration
.MinimumLevel.Is(logLevel)
#if DEBUG
.MinimumLevel.Override("Microsoft", LogEventLevel.Debug)
#else
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
#endif
.Enrich.FromLogContext()
.WriteTo.Console()
.ReadFrom.Configuration(context.Configuration);
})
.ConfigureServices(services =>
{
services.AddTransient(serviceProvider => serviceProvider.GetRequiredService<PluginManager>().CurrentHost?.Services.GetServices<IEventListener>() ?? Enumerable.Empty<IEventListener>());
var apiAssembly = typeof(IPlugin).Assembly;
var serilogAssembly = typeof(Serilog.ILogger).Assembly;
services.AddSingleton(new ServiceProxyCollection(
services
.Where(t => t.ServiceType.Assembly == apiAssembly || t.ServiceType.Assembly == serilogAssembly || t.ServiceType == typeof(PluginManager) || t.ServiceType == typeof(ILoggerFactory))
.Select(t => new ServiceRegistration(t.ServiceType, t.Lifetime))
.Distinct()
.ToArray()));
});
}
}
}
+32
View File
@@ -13,6 +13,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server", "Impostor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Api", "Impostor.Api\Impostor.Api.csproj", "{E096A7D7-D693-4A13-A526-38CC574D84F8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "patcher", "patcher", "{94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Shared", "Impostor.Patcher\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj", "{7C3EB599-2292-4532-B280-D5BED1094DD4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.WinForms", "Impostor.Patcher\Impostor.Patcher.WinForms\Impostor.Patcher.WinForms.csproj", "{804CF172-0C87-4423-9688-BD97D549891E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Cli", "Impostor.Patcher\Impostor.Patcher.Cli\Impostor.Patcher.Cli.csproj", "{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger", "Impostor.Plugins.Debugger\Impostor.Plugins.Debugger.csproj", "{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}"
@@ -63,6 +71,11 @@ Global
{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|Any CPU.Build.0 = Release|Any CPU
{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.ActiveCfg = Release|Any CPU
{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.Build.0 = Release|Any CPU
{804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{804CF172-0C87-4423-9688-BD97D549891E}.Debug|x86.ActiveCfg = Debug|Any CPU
{804CF172-0C87-4423-9688-BD97D549891E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{804CF172-0C87-4423-9688-BD97D549891E}.Release|x86.ActiveCfg = Release|Any CPU
{804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -71,6 +84,22 @@ Global
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU
{E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.ActiveCfg = Debug|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.Build.0 = Debug|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.Build.0 = Release|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.ActiveCfg = Release|Any CPU
{7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.Build.0 = Release|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.ActiveCfg = Debug|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.Build.0 = Debug|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.Build.0 = Release|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.ActiveCfg = Release|Any CPU
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.Build.0 = Release|Any CPU
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -136,6 +165,9 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D} = {56DD9707-D811-4056-9E2C-8A9CC2479B07}
{804CF172-0C87-4423-9688-BD97D549891E} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
{7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
{3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F}