{
var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..\\LocalLow");
var regionFile = Path.Combine(appData, "Innersloth", "Among Us", "regionInfo.dat");
- var region = new RegionInfo("Private", "127.0.0.1", new []
+ var region = new RegionInfo("Private", "192.168.1.211", new []
{
- new ServerInfo("Private-Master-1", "127.0.0.1", 22023)
+ new ServerInfo("Private-Master-1", "192.168.1.211", 22023)
});
using (var file = File.Open(regionFile, FileMode.Create, FileAccess.Write))
--- /dev/null
+namespace AmongUs.Server.Data
+{
+ public static class DisconnectMessages
+ {
+ public const string Error = "There was an internal server error. " +
+ "Check the server console for more information. " +
+ "Please report the issue on the AmongUsServer GitHub if it keeps happening.";
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Runtime.Serialization;
+
+namespace AmongUs.Server.Exceptions
+{
+ public class AmongUsException : Exception
+ {
+ public AmongUsException()
+ {
+ }
+
+ protected AmongUsException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public AmongUsException(string? message) : base(message)
+ {
+ }
+
+ public AmongUsException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
using System;
-using System.Text.Json;
+using AmongUs.Server.Data;
+using AmongUs.Server.Exceptions;
using AmongUs.Server.Extensions;
using AmongUs.Server.Net.Request;
using AmongUs.Server.Net.Response;
private readonly ClientManager _clientManager;
private readonly GameManager _gameManager;
- private readonly Connection _connection;
- private readonly ClientState _state;
- private readonly int _version;
- private readonly string _name;
- public Client(ClientManager clientManager, GameManager gameManager, Connection connection, int version, string name)
+ public Client(ClientManager clientManager, GameManager gameManager, int id, string name, Connection connection)
{
_clientManager = clientManager;
_gameManager = gameManager;
- _connection = connection;
- _connection.DataReceived += OnDataReceived;
- _connection.Disconnected += OnDisconnected;
- _state = new ClientState(_connection);
- _version = version;
- _name = name;
+ Id = id;
+ Name = name;
+ Connection = connection;
+ Connection.DataReceived += OnDataReceived;
+ Connection.Disconnected += OnDisconnected;
+ Player = new ClientPlayer(this);
}
+ public int Id { get; }
+ public string Name { get; }
+ public Connection Connection { get; }
+ public ClientPlayer Player { get; }
+
private void OnDataReceived(DataReceivedEventArgs e)
{
- while (true)
+ try
{
- if (e.Message.Position >= e.Message.Length)
+ while (true)
{
- break;
+ if (e.Message.Position >= e.Message.Length)
+ {
+ break;
+ }
+
+ OnMessageReceived(e.Message.ReadMessage(), e.SendOption);
}
-
- OnMessageReceived(e.Message.ReadMessage());
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Exception caught in client data handler.");
+ Connection.Send(new Message1DisconnectReason(DisconnectReason.Custom, DisconnectMessages.Error));
}
}
- private void OnMessageReceived(MessageReader message)
+ private void OnMessageReceived(MessageReader message, SendOption sendOption)
{
var flag = (RequestFlag) message.Tag;
+ Logger.Verbose("[{0}] Server got {1}.", Id, flag);
+
switch (flag)
{
- // 101A3813
case RequestFlag.HostGame:
- Logger.Debug("Server got host game");
-
+ {
// Read game settings.
var gameInfoBytes = message.ReadBytesAndSize();
var gameInfo = GameOptionsData.Deserialize(gameInfoBytes);
var game = _gameManager.Create(this, gameInfo);
if (game == null)
{
- _connection.Send(new Message1DisconnectReason(DisconnectReason.ServerFull));
+ Connection.Send(new Message1DisconnectReason(DisconnectReason.ServerFull));
return;
}
- // Code (32) in the packet below will be used in JoinGame.
+ // Code in the packet below will be used in JoinGame.
using (var writer = MessageWriter.Get(SendOption.Reliable))
{
writer.StartMessage(0);
- writer.Write(32);
+ writer.Write(game.Code);
writer.EndMessage();
- _connection.Send(writer);
+ Connection.Send(writer);
}
break;
- // 101A388C
+ }
+
case RequestFlag.JoinGame:
- Logger.Debug("Server got join game");
-
+ {
var gameCode = message.ReadInt32();
- if (gameCode != 32)
+ var unknown = message.ReadByte();
+ var game = _gameManager.Find(gameCode);
+ if (game == null)
{
- Logger.Debug("- Code {0}", gameCode, GameCode.IntToGameName(gameCode));
-
- _connection.Send(new Message1DisconnectReason(DisconnectReason.GameMissing));
+ Connection.Send(new Message1DisconnectReason(DisconnectReason.GameMissing));
return;
}
-
- // TODO: JoinGame
- Logger.Debug("JoinGame {0} {1}", gameCode, message.ReadByte());
+
+ game.HandleJoinGame(Player);
break;
+ }
+
// 101A3960
case RequestFlag.StartGame:
- Logger.Debug("Server got StartGame");
break;
+
// 101A39EC
case RequestFlag.RemoveGame:
- Logger.Debug("Server got RemoveGame");
break;
+
case RequestFlag.RemovePlayer:
- Logger.Debug("Server got RemovePlayer");
break;
- // 101A3A15
+
case RequestFlag.GameData:
- Logger.Debug("Server got GameData");
- break;
- // 101A3AAB
case RequestFlag.GameDataTo:
- Logger.Debug("Server got GameDataTo");
+ {
+ var game = Player.Game;
+ if (game == null)
+ {
+ throw new NullReferenceException("Game was not set for the client.");
+ }
+
+ var code = message.ReadInt32();
+ if (code != game.Code)
+ {
+ // Packet was meant for another game.
+ return;
+ }
+
+ // Broadcast packet to all other players.
+ using (var writer = MessageWriter.Get(sendOption))
+ {
+ if (flag == RequestFlag.GameDataTo)
+ {
+ var target = message.ReadPackedInt32();
+ writer.CopyFrom(message);
+ game.SendTo(writer, target);
+ }
+ else
+ {
+ writer.CopyFrom(message);
+ game.SendToAllExcept(writer, Player);
+ }
+ }
break;
+ }
+
// 101A3BA6
case RequestFlag.JoinedGame:
- Logger.Debug("Server got JoinedGame");
break;
+
// 101A3BD0
case RequestFlag.EndGame:
- Logger.Debug("Server got EndGame");
break;
+
default:
- Logger.Debug("Server received unknown {0}", flag);
+ Logger.Warning("Server received unknown flag {0}.", flag);
break;
}
- if (message.Position < message.Length)
+ if (flag != RequestFlag.GameData &&
+ flag != RequestFlag.GameDataTo &&
+ message.Position < message.Length)
{
Logger.Warning("Server did not consume all bytes from {0} ({1} < {2}).",
flag,
-using System.Collections.Generic;
+using System.Collections.Concurrent;
+using AmongUs.Server.Exceptions;
using Serilog;
namespace AmongUs.Server.Net
{
private static readonly ILogger Logger = Log.ForContext<ClientManager>();
- private readonly HashSet<Client> _clients;
+ private readonly ConcurrentDictionary<int, Client> _clients;
+ private readonly object _idLock;
+ private int _idLast;
public ClientManager()
{
- _clients = new HashSet<Client>();
+ _clients = new ConcurrentDictionary<int, Client>();
+ _idLock = new object();
+ _idLast = 0;
+ }
+
+ // No idea what a good way for this is.
+ public int NextId()
+ {
+ lock (_idLock)
+ {
+ // 3 Attempts.
+ for (var i = 0; i < 3; i++)
+ {
+ // It is important that ids start from 1, a 0 id causes issues.
+ var result = ++_idLast;
+
+ if (_idLast == int.MaxValue)
+ {
+ _idLast = 0;
+ }
+
+ if (_clients.ContainsKey(_idLast))
+ {
+ continue;
+ }
+
+ return result;
+ }
+
+ throw new AmongUsException("Unable to generate a client id.");
+ }
}
public void Add(Client client)
{
Logger.Information("Client connected.");
-
- _clients.Add(client);
+
+ _clients.TryAdd(client.Id, client);
}
public void Remove(Client client)
{
Logger.Information("Client disconnected.");
-
- _clients.Remove(client);
+
+ _clients.TryRemove(client.Id, out _);
}
}
}
\ No newline at end of file
--- /dev/null
+using AmongUs.Shared.Innersloth.Data;
+
+namespace AmongUs.Server.Net
+{
+ public class ClientPlayer
+ {
+ public ClientPlayer(Client client)
+ {
+ Client = client;
+ }
+
+ public Client Client { get; }
+ public Game Game { get; set; }
+ public LimboStates LimboState { get; set; }
+ }
+}
\ No newline at end of file
+++ /dev/null
-using Hazel;
-
-namespace AmongUs.Server.Net
-{
- public class ClientState
- {
- private readonly Connection _connection;
-
- public ClientState(Connection connection)
- {
- _connection = connection;
- }
- }
-}
\ No newline at end of file
-using AmongUs.Shared.Innersloth;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using AmongUs.Server.Exceptions;
+using AmongUs.Server.Extensions;
+using AmongUs.Server.Net.Response;
+using AmongUs.Shared.Innersloth;
+using AmongUs.Shared.Innersloth.Data;
+using Hazel;
+using Serilog;
+using ILogger = Serilog.ILogger;
namespace AmongUs.Server.Net
{
public class Game
{
+ private static readonly ILogger Logger = Log.ForContext<Game>();
+
+ private readonly ConcurrentDictionary<int, ClientPlayer> _players;
+ private int _hostId;
+
public Game(int code, GameOptionsData options)
{
Code = code;
+ CodeStr = GameCode.IntToGameName(code);
+ GameState = GameStates.NotStarted;
Options = options;
+
+ _hostId = -1;
+ _players = new ConcurrentDictionary<int, ClientPlayer>();
}
public int Code { get; }
+ public string CodeStr { get; }
+ public GameStates GameState { get; }
public GameOptionsData Options { get; }
+
+ public void SendToAllExcept(MessageWriter message, ClientPlayer sender)
+ {
+ foreach (var (_, player) in _players.Where(x => x.Value != sender))
+ {
+ player.Client.Connection.Send(message);
+ }
+ }
+
+ public void SendTo(MessageWriter message, int playerId)
+ {
+ if (_players.TryGetValue(playerId, out var player))
+ {
+ player.Client.Connection.Send(message);
+ }
+ else
+ {
+ Logger.Warning("[{0}] Sending data to {1} failed, player does not exist.", CodeStr, playerId);
+ }
+ }
+
+ public void HandleJoinGame(ClientPlayer player)
+ {
+ switch (GameState)
+ {
+ case GameStates.NotStarted:
+ HandleJoinGameNew(player);
+ break;
+ case GameStates.Started:
+ HandleJoinGameNext(player);
+ break;
+ case GameStates.Ended:
+ case GameStates.Destroyed:
+ player.Client.Connection.Send(new Message1DisconnectReason(DisconnectReason.GameStarted));
+ return;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void HandleJoinGameNew(ClientPlayer player)
+ {
+ Logger.Verbose("[{0}] Player joined.", CodeStr);
+
+ // Store player.
+ if (!_players.TryAdd(player.Client.Id, player))
+ {
+ throw new AmongUsException("Failed to add player to game.");
+ }
+
+ // Assign player to this game for future packets.
+ player.Game = this;
+
+ // Assign hostId if none is set.
+ if (_hostId == -1)
+ {
+ _hostId = player.Client.Id;
+ }
+
+ if (_hostId == player.Client.Id)
+ {
+ player.LimboState = LimboStates.NotLimbo;
+ }
+
+ using (var message = MessageWriter.Get(SendOption.Reliable))
+ {
+ // TODO: WriteJoinedMessage - Move to own method / class
+ message.StartMessage(7);
+ message.Write(Code);
+ message.Write(player.Client.Id);
+ message.Write(_hostId);
+ message.WritePacked(_players.Count - 1);
+
+ foreach (var (_, p) in _players.Where(x => x.Value == player))
+ {
+ message.WritePacked(p.Client.Id);
+ }
+
+ message.EndMessage();
+
+ message.StartMessage(10);
+ message.Write(Code);
+ message.Write((sbyte)1);
+ message.Write(false); // Private / Public
+ message.EndMessage();
+
+ player.Client.Connection.Send(message);
+
+ // TODO: BroadcastJoinMessage - Move to own method / class
+ message.Clear(SendOption.Reliable);
+ message.StartMessage(1);
+ message.Write(Code);
+ message.Write(player.Client.Id);
+ message.Write(_hostId);
+ message.EndMessage();
+
+ SendToAllExcept(message, player);
+ }
+ }
+
+ private void HandleJoinGameNext(ClientPlayer player)
+ {
+ throw new NotImplementedException();
+ }
}
}
\ No newline at end of file
return null;
}
- public void Remove(int key)
+ public Game Find(int gameCode)
{
- _games.TryRemove(key, out _);
+ _games.TryGetValue(gameCode, out var game);
+ return game;
+ }
+
+ public void Remove(int gameCode)
+ {
+ _games.TryRemove(gameCode, out _);
}
}
}
\ No newline at end of file
--- /dev/null
+namespace AmongUs.Server.Net
+{
+ public enum GameStates : byte
+ {
+ NotStarted = 0,
+ Started = 1,
+ Ended = 2,
+ Destroyed = 3
+ }
+}
\ No newline at end of file
}
// Register client.
- _clientManager.Add(new Client(_clientManager, _gameManager, e.Connection, clientVersion, clientName));
+ _clientManager.Add(new Client(_clientManager, _gameManager, _clientManager.NextId(), clientName, e.Connection));
}
public void Start()
public class Message1DisconnectReason : MessageBase
{
private readonly DisconnectReason _reason;
+ private readonly string _message;
// Notes:
// - Specifying no reason does something with ban minutes left.
// - (?) You were disconnected because Among Us was suspended by another app.
- public Message1DisconnectReason(DisconnectReason reason) : base(SendOption.Reliable, MessageFlag.DisconnectReason)
+ public Message1DisconnectReason(DisconnectReason reason, string message = null) : base(SendOption.Reliable, MessageFlag.DisconnectReason)
{
_reason = reason;
+ _message = message;
}
protected override void WriteMessage(MessageWriter writer)
{
writer.Write((int) _reason);
+
+ if (_reason == DisconnectReason.Custom)
+ {
+ writer.Write(_message);
+ }
}
}
}
\ No newline at end of file
--- /dev/null
+namespace AmongUs.Shared.Innersloth.Data
+{
+ public enum LimboStates
+ {
+ PreSpawn,
+ NotLimbo,
+ WaitingForHost,
+ }
+}
\ No newline at end of file