Compare commits

..

No commits in common. "master" and "1.0.2" have entirely different histories.

17 changed files with 280 additions and 1356 deletions

View File

@ -1,4 +0,0 @@
/Server/bin/
/Server/obj/
/Shared/bin/
/Shared/obj/

View File

@ -73,35 +73,3 @@ jobs:
platforms : linux/amd64,linux/arm/v7,linux/arm64/v8
cache-from : type=gha,scope=${{ github.workflow }}
cache-to : type=gha,scope=${{ github.workflow }},mode=max
-
name: Build binary files
run: |
./docker-build.sh all
-
name : Upload Server
uses : actions/upload-artifact@v3
with:
name : Server
path : ./bin/Server
if-no-files-found : error
-
name : Upload Server.arm
uses : actions/upload-artifact@v3
with:
name : Server.arm
path : ./bin/Server.arm
if-no-files-found : error
-
name : Upload Server.arm64
uses : actions/upload-artifact@v3
with:
name : Server.arm64
path : ./bin/Server.arm64
if-no-files-found : error
-
name : Upload Server.exe
uses : actions/upload-artifact@v3
with:
name : Server.exe
path : ./bin/Server.exe
if-no-files-found : error

3
.gitignore vendored
View File

@ -6,6 +6,3 @@ riderModule.iml
.idea/
settings.json
.vs/
/cache/
/data/

View File

@ -5,8 +5,8 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 as build
WORKDIR /app/
COPY ./Shared/Shared.csproj ./Shared/Shared.csproj
COPY ./Server/Server.csproj ./Server/Server.csproj
COPY ./Server/ ./Server/
COPY ./Shared/ ./Shared/
ARG TARGETARCH
@ -16,9 +16,6 @@ RUN dotnet restore \
-r debian.11-`echo $TARGETARCH | sed 's@^amd@x@'` \
;
COPY ./Shared/ ./Shared/
COPY ./Server/ ./Server/
# Build application binary
RUN dotnet publish \
./Server/Server.csproj \

View File

@ -29,7 +29,6 @@ cp smo.serivce /etc/systemd/system/smo.service
# edit ExecStart to your path for the server executable and change WorkingDirectory to the server directory
chmod +x filepath to the server executable
systemctl enable --now smo.service
# for TTY access i would recommand conspy but there is also reptyr, chvt
```
## Run docker image
@ -79,10 +78,8 @@ Maxplayers: the max amount of players that can join, default: 8
Flip: flips the player upside down, defaults: enabled: true, pov: both
Scenario: sync's scenario's for all players on the server, default: false
Banlist: banned people are unable to join the server, default: false
PersistShines/Moons: Allows the server to remember moon progress across crashes/restarts
### Discord
Note: Token and LogChannel needs to a string puts "" around it
Token: the token of the bot you want to load into, default: null
Prefix: the bot prefix to be used, default: $
LogChannel: logs the server console to that channel, default: null

View File

@ -1,359 +0,0 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using Shared;
using Shared.Packet.Packets;
namespace Server;
using MUCH = Func<string[], (HashSet<string> failToFind, HashSet<Client> toActUpon, List<(string arg, IEnumerable<string> amb)> ambig)>;
public static class BanLists {
public static bool Enabled {
get {
return Settings.Instance.BanList.Enabled;
}
private set {
Settings.Instance.BanList.Enabled = value;
}
}
private static ISet<string> IPs {
get {
return Settings.Instance.BanList.IpAddresses;
}
}
private static ISet<Guid> Profiles {
get {
return Settings.Instance.BanList.Players;
}
}
private static ISet<string> Stages {
get {
return Settings.Instance.BanList.Stages;
}
}
private static bool IsIPv4(string str) {
return IPAddress.TryParse(str, out IPAddress? ip)
&& ip != null
&& ip.AddressFamily == AddressFamily.InterNetwork;
;
}
public static bool IsIPv4Banned(Client user) {
IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint;
if (ipv4 == null) { return false; }
return IsIPv4Banned(ipv4.Address);
}
public static bool IsIPv4Banned(IPAddress ipv4) {
return IsIPv4Banned(ipv4.ToString());
}
public static bool IsIPv4Banned(string ipv4) {
return IPs.Contains(ipv4);
}
public static bool IsProfileBanned(Client user) {
return IsProfileBanned(user.Id);
}
public static bool IsProfileBanned(string str) {
if (!Guid.TryParse(str, out Guid id)) { return false; }
return IsProfileBanned(id);
}
public static bool IsProfileBanned(Guid id) {
return Profiles.Contains(id);
}
public static bool IsStageBanned(string stage) {
return Stages.Contains(stage);
}
public static bool IsClientBanned(Client user) {
return IsProfileBanned(user) || IsIPv4Banned(user);
}
private static void BanIPv4(Client user) {
IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint;
if (ipv4 != null) {
BanIPv4(ipv4.Address);
}
}
private static void BanIPv4(IPAddress ipv4) {
BanIPv4(ipv4.ToString());
}
private static void BanIPv4(string ipv4) {
IPs.Add(ipv4);
}
private static void BanProfile(Client user) {
BanProfile(user.Id);
}
private static void BanProfile(string str) {
if (!Guid.TryParse(str, out Guid id)) { return; }
BanProfile(id);
}
private static void BanProfile(Guid id) {
Profiles.Add(id);
}
private static void BanStage(string stage) {
Stages.Add(stage);
}
private static void BanClient(Client user) {
BanProfile(user);
BanIPv4(user);
}
private static void UnbanIPv4(Client user) {
IPEndPoint? ipv4 = (IPEndPoint?) user.Socket?.RemoteEndPoint;
if (ipv4 != null) {
UnbanIPv4(ipv4.Address);
}
}
private static void UnbanIPv4(IPAddress ipv4) {
UnbanIPv4(ipv4.ToString());
}
private static void UnbanIPv4(string ipv4) {
IPs.Remove(ipv4);
}
private static void UnbanProfile(Client user) {
UnbanProfile(user.Id);
}
private static void UnbanProfile(string str) {
if (!Guid.TryParse(str, out Guid id)) { return; }
UnbanProfile(id);
}
private static void UnbanProfile(Guid id) {
Profiles.Remove(id);
}
private static void UnbanStage(string stage) {
Stages.Remove(stage);
}
private static void Save() {
Settings.SaveSettings(true);
}
public static void Crash(
Client user,
int delay_ms = 0
) {
user.Ignored = true;
Task.Run(async () => {
if (delay_ms > 0) {
await Task.Delay(delay_ms);
}
bool permanent = user.Banned;
await user.Send(new ChangeStagePacket {
Id = (permanent ? "$agogus/ban4lyfe" : "$among$us/cr4sh%"),
Stage = (permanent ? "$ejected" : "$agogusStage"),
Scenario = (sbyte) (permanent ? 69 : 21),
SubScenarioType = (byte) (permanent ? 21 : 69),
});
});
}
private static void CrashMultiple(string[] args, MUCH much) {
foreach (Client user in much(args).toActUpon) {
user.Banned = true;
Crash(user);
}
}
public static string HandleBanCommand(string[] args, MUCH much) {
if (args.Length == 0) {
return "Usage: ban {list|enable|disable|player|profile|ip|stage} ...";
}
string cmd = args[0];
args = args.Skip(1).ToArray();
switch (cmd) {
default:
return "Usage: ban {list|enable|disable|player|profile|ip|stage} ...";
case "list":
if (args.Length != 0) {
return "Usage: ban list";
}
StringBuilder list = new StringBuilder();
list.Append("BanList: " + (Enabled ? "enabled" : "disabled"));
if (IPs.Count > 0) {
list.Append("\nBanned IPv4 addresses:\n- ");
list.Append(string.Join("\n- ", IPs));
}
if (Profiles.Count > 0) {
list.Append("\nBanned profile IDs:\n- ");
list.Append(string.Join("\n- ", Profiles));
}
if (Stages.Count > 0) {
list.Append("\nBanned stages:\n- ");
list.Append(string.Join("\n- ", Stages));
}
return list.ToString();
case "enable":
if (args.Length != 0) {
return "Usage: ban enable";
}
Enabled = true;
Save();
return "BanList enabled.";
case "disable":
if (args.Length != 0) {
return "Usage: ban disable";
}
Enabled = false;
Save();
return "BanList disabled.";
case "player":
if (args.Length == 0) {
return "Usage: ban player <* | !* (usernames to not ban...) | (usernames to ban...)>";
}
var res = much(args);
StringBuilder sb = new StringBuilder();
sb.Append(res.toActUpon.Count > 0 ? "Banned players: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : "");
sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : "");
if (res.ambig.Count > 0) {
res.ambig.ForEach(x => {
sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}");
});
}
foreach (Client user in res.toActUpon) {
user.Banned = true;
BanClient(user);
Crash(user);
}
Save();
return sb.ToString();
case "profile":
if (args.Length != 1) {
return "Usage: ban profile <profile-id>";
}
if (!Guid.TryParse(args[0], out Guid id)) {
return "Invalid profile ID value!";
}
if (IsProfileBanned(id)) {
return "Profile " + id.ToString() + " is already banned.";
}
BanProfile(id);
CrashMultiple(args, much);
Save();
return "Banned profile: " + id.ToString();
case "ip":
if (args.Length != 1) {
return "Usage: ban ip <ipv4-address>";
}
if (!IsIPv4(args[0])) {
return "Invalid IPv4 address!";
}
if (IsIPv4Banned(args[0])) {
return "IP " + args[0] + " is already banned.";
}
BanIPv4(args[0]);
CrashMultiple(args, much);
Save();
return "Banned ip: " + args[0];
case "stage":
if (args.Length != 1) {
return "Usage: ban stage <stage-name>";
}
string? stage = Shared.Stages.Input2Stage(args[0]);
if (stage == null) {
return "Invalid stage name!";
}
if (IsStageBanned(stage)) {
return "Stage " + stage + " is already banned.";
}
var stages = Shared.Stages
.StagesByInput(args[0])
.Where(s => !IsStageBanned(s))
.ToList()
;
foreach (string s in stages) {
BanStage(s);
}
Save();
return "Banned stage: " + string.Join(", ", stages);
}
}
public static string HandleUnbanCommand(string[] args) {
if (args.Length != 2) {
return "Usage: unban {profile|ip|stage} <value>";
}
string cmd = args[0];
string val = args[1];
switch (cmd) {
default:
return "Usage: unban {profile|ip|stage} <value>";
case "profile":
if (!Guid.TryParse(val, out Guid id)) {
return "Invalid profile ID value!";
}
if (!IsProfileBanned(id)) {
return "Profile " + id.ToString() + " is not banned.";
}
UnbanProfile(id);
Save();
return "Unbanned profile: " + id.ToString();
case "ip":
if (!IsIPv4(val)) {
return "Invalid IPv4 address!";
}
if (!IsIPv4Banned(val)) {
return "IP " + val + " is not banned.";
}
UnbanIPv4(val);
Save();
return "Unbanned ip: " + val;
case "stage":
string stage = Shared.Stages.Input2Stage(val) ?? val;
if (!IsStageBanned(stage)) {
return "Stage " + stage + " is not banned.";
}
var stages = Shared.Stages
.StagesByInput(val)
.Where(IsStageBanned)
.ToList()
;
foreach (string s in stages) {
UnbanStage(s);
}
Save();
return "Unbanned stage: " + string.Join(", ", stages);
}
}
}

View File

@ -12,8 +12,6 @@ namespace Server;
public class Client : IDisposable {
public readonly ConcurrentDictionary<string, object?> Metadata = new ConcurrentDictionary<string, object?>(); // can be used to store any information about a player
public bool Connected = false;
public bool Ignored = false;
public bool Banned = false;
public CostumePacket? CurrentCostume = null; // required for proper client sync
public string Name {
get => Logger.Name;
@ -22,7 +20,7 @@ public class Client : IDisposable {
public Guid Id;
public Socket? Socket;
public Server Server { get; init; } = null!; //init'd in object initializer
public Server Server { get; init; }
public Logger Logger { get; }
public Client(Socket socket) {
@ -30,21 +28,9 @@ public class Client : IDisposable {
Logger = new Logger("Unknown User");
}
// copy Client to use existing data for a new reconnected connection with a new socket
public Client(Client other, Socket socket) {
Metadata = other.Metadata;
Connected = other.Connected;
CurrentCostume = other.CurrentCostume;
Id = other.Id;
Socket = socket;
Server = other.Server;
Logger = other.Logger;
}
public void Dispose() {
if (Socket?.Connected is true) {
if (Socket?.Connected is true)
Socket.Disconnect(false);
}
}
@ -53,14 +39,9 @@ public class Client : IDisposable {
PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)];
try {
// don't send most packets to ignored players
if (Ignored && packetAttribute.Type != PacketType.Init && packetAttribute.Type != PacketType.ChangeStage) {
memory.Dispose();
return;
}
Server.FillPacket(new PacketHeader {
Id = sender?.Id ?? Id,
Type = packetAttribute.Type,
Id = sender?.Id ?? Id,
Type = packetAttribute.Type,
PacketSize = packet.Size
}, packet, memory.Memory);
}
@ -76,42 +57,14 @@ public class Client : IDisposable {
public async Task Send(Memory<byte> data, Client? sender) {
PacketHeader header = new PacketHeader();
header.Deserialize(data.Span);
if (!Connected && !Ignored && header.Type != PacketType.Connect) {
if (!Connected && header.Type is not PacketType.Connect) {
Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet");
return;
}
// don't send most packets to ignored players
if (Ignored && header.Type != PacketType.Init && header.Type != PacketType.ChangeStage) {
return;
}
await Socket!.SendAsync(data[..(Constants.HeaderSize + header.PacketSize)], SocketFlags.None);
}
public void CleanMetadataOnNewConnection() {
object? tmp;
Metadata.TryRemove("time", out tmp);
Metadata.TryRemove("seeking", out tmp);
Metadata.TryRemove("lastCostumePacket", out tmp);
Metadata.TryRemove("lastCapturePacket", out tmp);
Metadata.TryRemove("lastGamePacket", out tmp);
Metadata.TryRemove("lastPlayerPacket", out tmp);
}
public TagPacket? GetTagPacket() {
var time = (Time?) (this.Metadata.ContainsKey("time") ? this.Metadata["time"] : null);
var seek = (bool?) (this.Metadata.ContainsKey("seeking") ? this.Metadata["seeking"] : null);
if (time == null && seek == null) { return null; }
return new TagPacket {
UpdateType = (seek != null ? TagPacket.TagUpdate.State : 0) | (time != null ? TagPacket.TagUpdate.Time: 0),
IsIt = seek ?? false,
Seconds = (byte) (time?.Seconds ?? 0),
Minutes = (ushort) (time?.Minutes ?? 0),
};
}
public static bool operator ==(Client? left, Client? right) {
return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id;
}
@ -119,15 +72,4 @@ public class Client : IDisposable {
public static bool operator !=(Client? left, Client? right) {
return !(left == right);
}
public override bool Equals(object? obj) {
if (obj is Client)
return this == (Client)obj;
else
return false;
}
public override int GetHashCode() {
return Id.GetHashCode(); //relies upon same info as == operator.
}
}
}

View File

@ -11,7 +11,6 @@ public class DiscordBot {
private Settings.DiscordTable Config => Settings.Instance.Discord;
private string Prefix => Config.Prefix;
private readonly Logger Logger = new Logger("Discord");
private DiscordChannel? CommandChannel;
private DiscordChannel? LogChannel;
private bool Reconnecting;
@ -25,10 +24,6 @@ public class DiscordBot {
return "Restarting Discord bot";
});
if (Config.Token == null) return;
if (Config.CommandChannel == null)
Logger.Warn("You probably should set your CommandChannel in settings.json");
if (Config.LogChannel == null)
Logger.Warn("You probably should set your LogChannel in settings.json");
Settings.LoadHandler += SettingsLoadHandler;
}
@ -39,49 +34,23 @@ public class DiscordBot {
}
private async void SettingsLoadHandler() {
if (DiscordClient == null || Token != Config.Token) {
await Run();
try {
if (DiscordClient == null || Token != Config.Token)
await Run();
if (Config.LogChannel != null)
LogChannel = await (DiscordClient?.GetChannelAsync(ulong.Parse(Config.LogChannel)) ??
throw new NullReferenceException("Discord client not setup yet!"));
} catch (Exception e) {
Logger.Error($"Failed to get log channel \"{Config.LogChannel}\"");
Logger.Error(e);
}
if (DiscordClient == null) {
Logger.Error(new NullReferenceException("Discord client not setup yet!"));
return;
}
if (Config.CommandChannel != null) {
try {
CommandChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.CommandChannel));
} catch (Exception e) {
Logger.Error($"Failed to get command channel \"{Config.CommandChannel}\"");
Logger.Error(e);
}
}
if (Config.LogChannel != null) {
try {
LogChannel = await DiscordClient.GetChannelAsync(ulong.Parse(Config.LogChannel));
} catch (Exception e) {
Logger.Error($"Failed to get log channel \"{Config.LogChannel}\"");
Logger.Error(e);
}
}
}
private static List<string> SplitMessage(string message, int maxSizePerElem = 2000)
{
List<string> result = new List<string>();
for (int i = 0; i < message.Length; i += maxSizePerElem)
{
result.Add(message.Substring(i, message.Length - i < maxSizePerElem ? message.Length - i : maxSizePerElem));
}
return result;
}
private async void Log(string source, string level, string text, ConsoleColor _) {
try {
if (DiscordClient != null && LogChannel != null) {
foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1994)) //room for 6 '`'
await DiscordClient.SendMessageAsync(LogChannel, $"```{mesg}```");
await DiscordClient.SendMessageAsync(LogChannel,
$"```{Logger.PrefixNewLines(text, $"{level} [{source}]")}```");
}
} catch (Exception e) {
// don't log again, it'll just stack overflow the server!
@ -109,34 +78,23 @@ public class DiscordBot {
Logger.Info(
$"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}");
Reconnecting = false;
string mentionPrefix = $"{DiscordClient.CurrentUser.Mention}";
string mentionPrefix = $"{DiscordClient.CurrentUser.Mention} ";
DiscordClient.MessageCreated += async (_, args) => {
if (args.Author.IsCurrent) return; //dont respond to commands from ourselves (prevent "sql-injection" esq attacks)
//prevent commands via dm and non-public channels
if (CommandChannel == null) {
if (args.Channel is DiscordDmChannel)
return; //no dm'ing the bot allowed!
}
else if (args.Channel.Id != CommandChannel.Id && (LogChannel != null && args.Channel.Id != LogChannel.Id))
return;
//run command
if (args.Author.IsCurrent) return;
try {
DiscordMessage msg = args.Message;
string? resp = null;
if (string.IsNullOrEmpty(Prefix)) {
await msg.Channel.TriggerTypingAsync();
resp = string.Join('\n', CommandHandler.GetResult(msg.Content).ReturnStrings);
await msg.RespondAsync(string.Join('\n',
CommandHandler.GetResult(msg.Content).ReturnStrings));
} else if (msg.Content.StartsWith(Prefix)) {
await msg.Channel.TriggerTypingAsync();
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings);
await msg.RespondAsync(string.Join('\n',
CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings));
} else if (msg.Content.StartsWith(mentionPrefix)) {
await msg.Channel.TriggerTypingAsync();
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[mentionPrefix.Length..].TrimStart()).ReturnStrings);
}
if (resp != null)
{
foreach (string mesg in SplitMessage(resp))
await msg.RespondAsync(mesg);
await msg.RespondAsync(string.Join('\n',
CommandHandler.GetResult(msg.Content[mentionPrefix.Length..]).ReturnStrings));
}
} catch (Exception e) {
Logger.Error(e);
@ -157,4 +115,4 @@ public class DiscordBot {
Logger.Error(e);
}
}
}
}

View File

@ -1,4 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Net;
using System.Numerics;
using System.Text;
@ -11,7 +11,7 @@ using Timer = System.Timers.Timer;
Server.Server server = new Server.Server();
HashSet<int> shineBag = new HashSet<int>();
CancellationTokenSource cts = new CancellationTokenSource();
bool restartRequested = false;
Task listenTask = server.Listen(cts.Token);
Logger consoleLogger = new Logger("Console");
DiscordBot bot = new DiscordBot();
await bot.Run();
@ -48,10 +48,6 @@ async Task LoadShines()
if (loadedShines is not null) shineBag = loadedShines;
}
catch (FileNotFoundException)
{
// Ignore
}
catch (Exception ex)
{
consoleLogger.Error(ex);
@ -62,24 +58,34 @@ async Task LoadShines()
await LoadShines();
server.ClientJoined += (c, _) => {
if (Settings.Instance.BanList.Enabled
&& (Settings.Instance.BanList.Players.Contains(c.Id)
|| Settings.Instance.BanList.IpAddresses.Contains(
((IPEndPoint) c.Socket!.RemoteEndPoint!).Address.ToString())))
throw new Exception($"Banned player attempted join: {c.Name}");
c.Metadata["shineSync"] = new ConcurrentBag<int>();
c.Metadata["loadedSave"] = false;
c.Metadata["scenario"] = (byte?) 0;
c.Metadata["2d"] = false;
c.Metadata["speedrun"] = false;
foreach (Client client in server.ClientsConnected) {
try {
c.Send((GamePacket) client.Metadata["lastGamePacket"]!, client).Wait();
} catch {
// lol who gives a fuck
}
}
};
async Task ClientSyncShineBag(Client client) {
if (!Settings.Instance.Shines.Enabled) return;
try {
if ((bool?) client.Metadata["speedrun"] ?? false) return;
ConcurrentBag<int> clientBag = (ConcurrentBag<int>) (client.Metadata["shineSync"] ??= new ConcurrentBag<int>());
foreach (int shine in shineBag.Except(clientBag).Except(Settings.Instance.Shines.Excluded).ToArray()) {
if (!client.Connected) return;
foreach (int shine in shineBag.Except(clientBag).ToArray()) {
clientBag.Add(shine);
await client.Send(new ShinePacket {
ShineId = shine
});
clientBag.Add(shine);
}
} catch {
// errors that can happen when sending will crash the server :)
@ -89,7 +95,7 @@ async Task ClientSyncShineBag(Client client) {
async void SyncShineBag() {
try {
await PersistShines();
await Parallel.ForEachAsync(server.ClientsConnected.ToArray(), async (client, _) => await ClientSyncShineBag(client));
await Parallel.ForEachAsync(server.Clients.ToArray(), async (client, _) => await ClientSyncShineBag(client));
} catch {
// errors that can happen shines change will crash the server :)
}
@ -103,55 +109,21 @@ timer.Start();
float MarioSize(bool is2d) => is2d ? 180 : 160;
void flipPlayer(Client c, ref PlayerPacket pp) {
pp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]!);
pp.Rotation *= (
Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI))
);
};
void logError(Task x) {
if (x.Exception != null) {
consoleLogger.Error(x.Exception.ToString());
}
};
server.PacketHandler = (c, p) => {
switch (p) {
case GamePacket gamePacket: {
// crash ignored player
if (c.Ignored) {
c.Logger.Info($"Crashing ignored player after entering stage {gamePacket.Stage}.");
BanLists.Crash(c, 500);
return false;
}
// crash player entering a banned stage
if (BanLists.Enabled && BanLists.IsStageBanned(gamePacket.Stage)) {
c.Logger.Warn($"Crashing player for entering banned stage {gamePacket.Stage}.");
BanLists.Crash(c, 500);
return false;
}
c.Logger.Info($"Got game packet {gamePacket.Stage}->{gamePacket.ScenarioNum}");
// reset lastPlayerPacket on stage changes
object? old = null;
c.Metadata.TryGetValue("lastGamePacket", out old);
if (old != null && ((GamePacket) old).Stage != gamePacket.Stage) {
c.Metadata["lastPlayerPacket"] = null;
}
c.Metadata["scenario"] = gamePacket.ScenarioNum;
c.Metadata["2d"] = gamePacket.Is2d;
c.Metadata["lastGamePacket"] = gamePacket;
switch (gamePacket.Stage) {
case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0:
c.Metadata["speedrun"] = true;
((ConcurrentBag<int>) (c.Metadata["shineSync"] ??= new ConcurrentBag<int>())).Clear();
shineBag.Clear();
Task.Run(async () => {
await PersistShines();
});
c.Logger.Info("Entered Cap on new save, preventing moon sync until Cascade");
break;
case "WaterfallWorldHomeStage":
@ -169,54 +141,26 @@ server.PacketHandler = (c, p) => {
if (Settings.Instance.Scenario.MergeEnabled) {
server.BroadcastReplace(gamePacket, c, (from, to, gp) => {
gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200;
#pragma warning disable CS4014
to.Send(gp, from).ContinueWith(logError);
#pragma warning restore CS4014
to.Send(gp, from);
});
return false;
}
break;
}
// ignore all other packets from ignored players
case IPacket pack when c.Ignored: {
return false;
}
case TagPacket tagPacket: {
// c.Logger.Info($"Got tag packet: {tagPacket.IsIt}");
if ((tagPacket.UpdateType & TagPacket.TagUpdate.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt;
if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0)
c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now);
break;
}
case CapturePacket capturePacket: {
// c.Logger.Info($"Got capture packet: {capturePacket.ModelName}");
c.Metadata["lastCapturePacket"] = capturePacket;
break;
}
case CostumePacket costumePacket: {
c.Logger.Info($"Got costume packet: {costumePacket.BodyName}, {costumePacket.CapName}");
c.Metadata["lastCostumePacket"] = costumePacket;
c.CurrentCostume = costumePacket;
#pragma warning disable CS4014
ClientSyncShineBag(c); //no point logging since entire def has try/catch
#pragma warning restore CS4014
case CostumePacket:
ClientSyncShineBag(c);
c.Metadata["loadedSave"] = true;
break;
}
case ShinePacket shinePacket: {
if (!Settings.Instance.Shines.Enabled) return false;
if (Settings.Instance.Shines.Excluded.Contains(shinePacket.ShineId)) {
c.Logger.Info($"Got moon {shinePacket.ShineId} (excluded)");
return false;
}
if (c.Metadata["loadedSave"] is false) break;
ConcurrentBag<int> playerBag = (ConcurrentBag<int>)c.Metadata["shineSync"]!;
ConcurrentBag<int> playerBag = (ConcurrentBag<int>) c.Metadata["shineSync"];
shineBag.Add(shinePacket.ShineId);
if (playerBag.Contains(shinePacket.ShineId)) break;
c.Logger.Info($"Got moon {shinePacket.ShineId}");
@ -224,157 +168,135 @@ server.PacketHandler = (c, p) => {
SyncShineBag();
break;
}
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others
&& Settings.Instance.Flip.Players.Contains(c.Id): {
playerPacket.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]);
playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
server.Broadcast(playerPacket, c);
return false;
}
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self
&& !Settings.Instance.Flip.Players.Contains(c.Id): {
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
if (Settings.Instance.Flip.Players.Contains(to.Id)) {
sp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]);
sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
}
case PlayerPacket playerPacket: {
c.Metadata["lastPlayerPacket"] = playerPacket;
// flip for all
if ( Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Others
&& Settings.Instance.Flip.Players.Contains(c.Id)
) {
flipPlayer(c, ref playerPacket);
#pragma warning disable CS4014
server.Broadcast(playerPacket, c).ContinueWith(logError);
#pragma warning restore CS4014
return false;
}
// flip only for specific clients
if ( Settings.Instance.Flip.Enabled
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self
&& !Settings.Instance.Flip.Players.Contains(c.Id)
) {
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
if (Settings.Instance.Flip.Players.Contains(to.Id)) {
flipPlayer(c, ref sp);
}
#pragma warning disable CS4014
to.Send(sp, from).ContinueWith(logError);
#pragma warning restore CS4014
});
return false;
}
break;
to.Send(sp, from);
});
return false;
}
}
return true; // Broadcast packet to all other clients
return true;
};
(HashSet<string> failToFind, HashSet<Client> toActUpon, List<(string arg, IEnumerable<string> amb)> ambig) MultiUserCommandHelper(string[] args) {
HashSet<string> failToFind = new();
HashSet<Client> toActUpon;
List<(string arg, IEnumerable<string> amb)> ambig = new();
if (args[0] == "*") {
toActUpon = new(server.Clients.Where(c => c.Connected));
}
else {
toActUpon = args[0] == "!*" ? new(server.Clients.Where(c => c.Connected)) : new();
for (int i = (args[0] == "!*" ? 1 : 0); i < args.Length; i++) {
string arg = args[i];
IEnumerable<Client> search = server.Clients.Where(c => c.Connected && (
c.Name.ToLower().StartsWith(arg.ToLower())
|| (Guid.TryParse(arg, out Guid res) && res == c.Id)
|| (IPAddress.TryParse(arg, out IPAddress? ip) && ip.Equals(((IPEndPoint) c.Socket!.RemoteEndPoint!).Address))
));
if (!search.Any()) {
failToFind.Add(arg); //none found
}
else if (search.Count() > 1) {
Client? exact = search.FirstOrDefault(x => x.Name == arg);
if (!ReferenceEquals(exact, null)) {
//even though multiple matches, since exact match, it isn't ambiguous
if (args[0] == "!*") {
toActUpon.Remove(exact);
}
else {
toActUpon.Add(exact);
}
}
else {
if (!ambig.Any(x => x.arg == arg)) {
ambig.Add((arg, search.Select(x => x.Name))); //more than one match
}
foreach (var rem in search.ToList()) { //need copy because can't remove from list while iterating over it
toActUpon.Remove(rem);
}
}
}
else {
//only one match, so autocomplete
if (args[0] == "!*") {
toActUpon.Remove(search.First());
}
else {
toActUpon.Add(search.First());
}
}
}
}
return (failToFind, toActUpon, ambig);
}
CommandHandler.RegisterCommand("rejoin", args => {
if (args.Length == 0) {
return "Usage: rejoin <* | !* (usernames to not rejoin...) | (usernames to rejoin...)>";
if (args.Length == 0) {
return "Usage: rejoin <* | usernames...>";
}
var res = MultiUserCommandHelper(args);
StringBuilder sb = new StringBuilder();
sb.Append(res.toActUpon.Count > 0 ? "Rejoined: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : "");
sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : "");
if (res.ambig.Count > 0) {
res.ambig.ForEach(x => {
sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}");
});
}
foreach (Client user in res.toActUpon) {
bool moreThanOne = false;
StringBuilder builder = new StringBuilder();
Client[] clients = (args[0].Trim() == "*"
? server.Clients.Where(c => c.Connected)
: server.Clients.Where(c =>
c.Connected && args.Any(x => c.Name == x || (Guid.TryParse(x, out Guid result) && result == c.Id)))).ToArray();
foreach (Client user in clients) {
if (moreThanOne) builder.Append(", ");
builder.Append(user.Name);
user.Dispose();
moreThanOne = true;
}
return sb.ToString();
return clients.Length > 0 ? $"Caused {builder} to rejoin" : "Usage: rejoin <usernames...>";
});
CommandHandler.RegisterCommand("crash", args => {
if (args.Length == 0) {
return "Usage: crash <* | !* (usernames to not crash...) | (usernames to crash...)>";
if (args.Length == 0) {
return "Usage: crash <* | usernames...>";
}
var res = MultiUserCommandHelper(args);
StringBuilder sb = new StringBuilder();
sb.Append(res.toActUpon.Count > 0 ? "Crashed: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : "");
sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : "");
if (res.ambig.Count > 0) {
res.ambig.ForEach(x => {
sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}");
bool moreThanOne = false;
StringBuilder builder = new StringBuilder();
Client[] clients = (args[0].Trim() == "*"
? server.Clients.Where(c => c.Connected)
: server.Clients.Where(c =>
c.Connected && args.Any(x => c.Name == x || (Guid.TryParse(x, out Guid result) && result == c.Id)))).ToArray();
foreach (Client user in clients) {
if (moreThanOne) builder.Append(", ");
moreThanOne = true;
builder.Append(user.Name);
Task.Run(async () => {
await user.Send(new ChangeStagePacket {
Id = "$among$us/SubArea",
Stage = "$agogusStage",
Scenario = 21,
SubScenarioType = 69 // invalid id
});
user.Dispose();
});
}
foreach (Client user in res.toActUpon) {
BanLists.Crash(user);
}
return sb.ToString();
return clients.Length > 0 ? $"Crashed {builder}" : "Usage: crash <usernames...>";
});
CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); });
CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); });
CommandHandler.RegisterCommand("ban", args => {
if (args.Length == 0) {
return "Usage: ban <* | usernames...>";
}
bool moreThanOne = false;
StringBuilder builder = new StringBuilder();
Client[] clients = (args[0].Trim() == "*"
? server.Clients.Where(c => c.Connected)
: server.Clients.Where(c =>
c.Connected && args.Any(x => c.Name == x || (Guid.TryParse(x, out Guid result) && result == c.Id)))).ToArray();
foreach (Client user in clients) {
if (moreThanOne) builder.Append(", ");
moreThanOne = true;
builder.Append(user.Name);
Task.Run(async () => {
await user.Send(new ChangeStagePacket {
Id = "$agogus/banned4lyfe",
Stage = "$ejected",
Scenario = 69,
SubScenarioType = 21 // invalid id
});
IPEndPoint? endpoint = (IPEndPoint?) user.Socket?.RemoteEndPoint;
Settings.Instance.BanList.Players.Add(user.Id);
if (endpoint != null) Settings.Instance.BanList.IpAddresses.Add(endpoint.ToString());
user.Dispose();
});
}
if (clients.Length > 0) {
Settings.SaveSettings();
return $"Banned {builder}.";
}
return "Usage: ban <usernames...>";
});
CommandHandler.RegisterCommand("send", args => {
const string optionUsage = "Usage: send <stage> <id> <scenario[-1..127]> <player/*>";
if (args.Length < 4) {
if (args.Length < 4)
return optionUsage;
}
string? stage = Stages.Input2Stage(args[0]);
if (stage == null) {
return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```";
}
string stage = args[0];
string id = args[1];
if (Constants.MapNames.TryGetValue(stage.ToLower(), out string? mapName)) {
stage = mapName;
}
if (!stage.Contains("Stage") && !stage.Contains("Zone")) {
return "Invalid Stage Name! ```cap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side```";
}
if (!sbyte.TryParse(args[2], out sbyte scenario) || scenario < -1)
return $"Invalid scenario number {args[2]} (range: [-1 to 127])";
Client[] players = args[3] == "*"
@ -396,13 +318,17 @@ CommandHandler.RegisterCommand("send", args => {
CommandHandler.RegisterCommand("sendall", args => {
const string optionUsage = "Usage: sendall <stage>";
if (args.Length < 1) {
if (args.Length < 1)
return optionUsage;
string stage = args[0];
if (Constants.MapNames.TryGetValue(stage.ToLower(), out string? mapName)) {
stage = mapName;
}
string? stage = Stages.Input2Stage(args[0]);
if (stage == null) {
return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```";
if (!stage.Contains("Stage") && !stage.Contains("Zone")) {
return "Invalid Stage Name! ```cap -> Cap Kingdom\ncascade -> Cascade Kingdom\nsand -> Sand Kingdom\nlake -> Lake Kingdom\nwooded -> Wooded Kingdom\ncloud -> Cloud Kingdom\nlost -> Lost Kingdom\nmetro -> Metro Kingdom\nsea -> Sea Kingdom\nsnow -> Snow Kingdom\nlunch -> Luncheon Kingdom\nruined -> Ruined Kingdom\nbowser -> Bowser's Kingdom\nmoon -> Moon Kingdom\nmush -> Mushroom Kingdom\ndark -> Dark Side\ndarker -> Darker Side```";
}
Client[] players = server.Clients.Where(c => c.Connected).ToArray();
@ -577,24 +503,16 @@ CommandHandler.RegisterCommand("flip", args => {
});
CommandHandler.RegisterCommand("shine", args => {
const string optionUsage = "Valid options: list, clear, sync, send, set, include, exclude";
const string optionUsage = "Valid options: list, clear, sync, send";
if (args.Length < 1)
return optionUsage;
switch (args[0]) {
case "list" when args.Length == 1:
return $"Shines: {string.Join(", ", shineBag)}" + (
Settings.Instance.Shines.Excluded.Count() > 0
? "\nExcluded Shines: " + string.Join(", ", Settings.Instance.Shines.Excluded)
: ""
);
return $"Shines: {string.Join(", ", shineBag)}";
case "clear" when args.Length == 1:
shineBag.Clear();
Task.Run(async () => {
await PersistShines();
});
foreach (ConcurrentBag<int> playerBag in server.Clients.Select(serverClient =>
(ConcurrentBag<int>)serverClient.Metadata["shineSync"]!)) playerBag?.Clear();
(ConcurrentBag<int>) serverClient.Metadata["shineSync"])) playerBag.Clear();
return "Cleared shine bags";
case "sync" when args.Length == 1:
@ -614,30 +532,6 @@ CommandHandler.RegisterCommand("shine", args => {
}
return optionUsage;
case "set" when args.Length == 2: {
if (bool.TryParse(args[1], out bool result)) {
Settings.Instance.Shines.Enabled = result;
Settings.SaveSettings();
return result ? "Enabled shine sync" : "Disabled shine sync";
}
return optionUsage;
}
case "exclude" when args.Length == 2:
case "include" when args.Length == 2: {
if (int.TryParse(args[1], out int sid)) {
if (args[0] == "exclude") {
Settings.Instance.Shines.Excluded.Add(sid);
Settings.SaveSettings();
return $"Exclude shine {sid} from syncing.";
} else {
Settings.Instance.Shines.Excluded.Remove(sid);
Settings.SaveSettings();
return $"No longer exclude shine {sid} from syncing.";
}
}
return optionUsage;
}
default:
return optionUsage;
}
@ -648,21 +542,6 @@ CommandHandler.RegisterCommand("loadsettings", _ => {
return "Loaded settings.json";
});
CommandHandler.RegisterCommand("restartserver", args =>
{
if (args.Length != 0)
{
return "Usage: restartserver (no arguments)";
}
else
{
consoleLogger.Info("Received restartserver command");
restartRequested = true;
cts.Cancel();
return "Restarting...";
}
});
Console.CancelKeyPress += (_, e) => {
e.Cancel = true;
consoleLogger.Info("Received Ctrl+C");
@ -674,7 +553,6 @@ CommandHandler.RegisterCommandAliases(_ => {
return "Shutting down";
}, "exit", "quit", "q");
#pragma warning disable CS4014
Task.Run(() => {
consoleLogger.Info("Run help command for valid commands.");
while (true) {
@ -685,20 +563,6 @@ Task.Run(() => {
}
}
}
}).ContinueWith(logError);
#pragma warning restore CS4014
});
await server.Listen(cts.Token);
if (restartRequested) //need to do this here because this needs to happen after the listener closes, and there isn't an
//easy way to sync in the restartserver command without it exiting Main()
{
string? path = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
const string unableToStartMsg = "Unable to ascertain the executable location, you'll need to re-run the server manually.";
if (path != null) //path is probably just "Server", but in the context of the assembly, that's all you need to restart it.
{
Console.WriteLine($"Server Running on (pid): {System.Diagnostics.Process.Start(path)?.Id.ToString() ?? unableToStartMsg}");
}
else
consoleLogger.Info(unableToStartMsg);
}
await listenTask;

View File

@ -31,12 +31,8 @@ public class Server {
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
// start sub thread to handle client
try {
#pragma warning disable CS4014
Task.Run(() => HandleSocket(socket))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
Task.Run(() => HandleSocket(socket));
}
catch (Exception e) {
Logger.Error($"Error occured while setting up socket handler? {e}");
@ -59,13 +55,12 @@ public class Server {
}
Logger.Info("Server closed");
Console.WriteLine("\n\n\n"); //for the sake of the restart command.
}
}
public static void FillPacket<T>(PacketHeader header, T packet, Memory<byte> memory) where T : struct, IPacket {
Span<byte> data = memory.Span;
header.Serialize(data[..Constants.HeaderSize]);
packet.Serialize(data[Constants.HeaderSize..]);
}
@ -74,29 +69,27 @@ public class Server {
public delegate void PacketReplacer<in T>(Client from, Client to, T value); // replacer must send
public void BroadcastReplace<T>(T packet, Client sender, PacketReplacer<T> packetReplacer) where T : struct, IPacket {
foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.Id)) {
packetReplacer(sender, client, packet);
}
foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet);
}
public async Task Broadcast<T>(T packet, Client sender) where T : struct, IPacket {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
PacketHeader header = new PacketHeader {
Id = sender?.Id ?? Guid.Empty,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size,
Id = sender?.Id ?? Guid.Empty,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size
};
FillPacket(header, packet, memory.Memory);
await Broadcast(memory, sender);
}
public Task Broadcast<T>(T packet) where T : struct, IPacket {
return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).Select(async client => {
return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
PacketHeader header = new PacketHeader {
Id = client.Id,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size,
Id = client.Id,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size
};
FillPacket(header, packet, memory.Memory);
await client.Send(memory.Memory, client);
@ -110,7 +103,7 @@ public class Server {
/// <param name="data">Memory owner to dispose once done</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async Task Broadcast(IMemoryOwner<byte> data, Client? sender = null) {
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender)));
await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory, sender)));
data.Dispose();
}
@ -120,7 +113,7 @@ public class Server {
/// <param name="data">Memory to send to the clients</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async void Broadcast(Memory<byte> data, Client? sender = null) {
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender)));
await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data, sender)));
}
public Client? FindExistingClient(Guid id) {
@ -130,9 +123,10 @@ public class Server {
private async void HandleSocket(Socket socket) {
Client client = new Client(socket) {Server = this};
var remote = socket.RemoteEndPoint;
IMemoryOwner<byte> memory = null!;
await client.Send(new InitPacket {
MaxPlayers = Settings.Instance.Server.MaxPlayers
});
bool first = true;
try {
while (true) {
@ -144,7 +138,7 @@ public class Server {
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
if (size == 0) {
// treat it as a disconnect and exit
Logger.Info($"Socket {remote} disconnected.");
Logger.Info($"Socket {socket.RemoteEndPoint} disconnected.");
if (socket.Connected) await socket.DisconnectAsync(false);
return false;
}
@ -155,9 +149,8 @@ public class Server {
return true;
}
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) {
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0))
break;
}
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
if (header.PacketSize > 0) {
@ -165,101 +158,67 @@ public class Server {
memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize);
memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]);
memTemp.Dispose();
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) {
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize))
break;
}
}
// connection initialization
if (first) {
first = false; // only do this once
// first client packet has to be the client init
if (header.Type != PacketType.Connect) {
throw new Exception($"First packet was not init, instead it was {header.Type} ({remote})");
}
first = false;
if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
ConnectPacket connect = new ConnectPacket();
connect.Deserialize(memory.Memory.Span[packetRange]);
client.Id = header.Id;
client.Name = connect.ClientName;
// is the IPv4 address banned?
if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) {
Logger.Warn($"Ignoring banned IPv4 address for {client.Name} ({client.Id}/{remote})");
client.Ignored = true;
client.Banned = true;
}
// is the profile ID banned?
else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) {
client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})");
client.Ignored = true;
client.Banned = true;
}
// is the server full?
else if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}");
client.Ignored = true;
}
// send server init (required to crash ignored players later)
await client.Send(new InitPacket {
MaxPlayers = (client.Ignored ? (ushort) 1 : Settings.Instance.Server.MaxPlayers),
});
// don't init or announce an ignored client to other players any further
if (client.Ignored) {
memory.Dispose();
continue;
}
bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection;
// add client to the set of connected players
lock (Clients) {
// is the server full? (check again, to prevent race conditions)
if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}");
client.Ignored = true;
client.Name = connect.ClientName;
if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Turned away as server is at max clients");
memory.Dispose();
continue;
goto disconnect;
}
// detect and handle reconnections
bool isClientNew = true;
bool firstConn = false;
switch (connect.ConnectionType) {
case ConnectPacket.ConnectionTypes.FirstConnection:
case ConnectPacket.ConnectionTypes.Reconnecting: {
if (FindExistingClient(client.Id) is { } oldClient) {
isClientNew = false;
client = new Client(oldClient, socket);
client.Name = connect.ClientName;
Clients.Remove(oldClient);
Clients.Add(client);
if (oldClient.Connected) {
oldClient.Logger.Info($"Disconnecting already connected client {oldClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
oldClient.Dispose();
case ConnectPacket.ConnectionTypes.FirstConnection: {
firstConn = true;
if (FindExistingClient(header.Id) is { } newClient) {
if (newClient.Connected) {
newClient.Logger.Info($"Disconnecting already connected client {newClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
newClient.Dispose();
}
newClient.Socket = client.Socket;
client = newClient;
}
else {
break;
}
case ConnectPacket.ConnectionTypes.Reconnecting: {
client.Id = header.Id;
if (FindExistingClient(header.Id) is { } newClient) {
if (newClient.Connected) {
newClient.Logger.Info($"Disconnecting already connected client {newClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
newClient.Dispose();
}
newClient.Socket = client.Socket;
client = newClient;
} else {
firstConn = true;
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
}
break;
}
default: {
throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})");
}
default:
throw new Exception($"Invalid connection type {connect.ConnectionType}");
}
client.Connected = true;
if (isClientNew) {
if (firstConn) {
// do any cleanup required when it comes to new clients
List<Client> toDisconnect = Clients.FindAll(c => c.Id == client.Id && c.Connected && c.Socket != null);
Clients.RemoveAll(c => c.Id == client.Id);
List<Client> toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null);
Clients.RemoveAll(c => c.Id == header.Id);
client.Id = header.Id;
Clients.Add(client);
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
@ -267,35 +226,26 @@ public class Server {
ClientJoined?.Invoke(client, connect);
}
// a known client reconnects, but with a new first connection (e.g. after a restart)
else if (wasFirst) {
client.CleanMetadataOnNewConnection();
}
}
// for all other clients that are already connected
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != client.Id && c.Connected && c.Socket != null);
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size));
// make the other client known to the new client
PacketHeader connectHeader = new PacketHeader {
Id = other.Id,
Type = PacketType.Connect,
PacketSize = connect.Size,
Id = other.Id,
Type = PacketType.Connect,
PacketSize = connect.Size
};
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
ConnectPacket connectPacket = new ConnectPacket {
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
MaxPlayers = Settings.Instance.Server.MaxPlayers,
ClientName = other.Name,
MaxPlayers = Settings.Instance.Server.MaxPlayers,
ClientName = other.Name
};
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
// tell the new client what costume the other client has
if (other.CurrentCostume.HasValue) {
connectHeader.Type = PacketType.Costume;
connectHeader.Type = PacketType.Costume;
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]);
@ -303,30 +253,25 @@ public class Server {
}
tempBuffer.Dispose();
// make the other client reset their puppet cache for this new client, if it is a new connection (after restart)
if (wasFirst) {
await SendEmptyPackets(client, other);
}
});
Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected.");
// send missing or outdated packets from others to the new client
await ResendPackets(client);
}
else if (header.Id != client.Id && client.Id != Guid.Empty) {
Logger.Info($"Client {client.Name} ({client.Id}/{socket.RemoteEndPoint}) connected.");
} else if (header.Id != client.Id && client.Id != Guid.Empty) {
throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}");
}
if (header.Type == PacketType.Costume) {
CostumePacket costumePacket = new CostumePacket {
BodyName = ""
};
costumePacket.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + costumePacket.Size)]);
client.CurrentCostume = costumePacket;
}
try {
// parse the packet
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
// process the packet
if (PacketHandler?.Invoke(client, packet) is false) {
// don't broadcast the packet to everyone
memory.Dispose();
continue;
}
@ -335,87 +280,31 @@ public class Server {
client.Logger.Error($"Packet handler warning: {e}");
}
#pragma warning disable CS4014
// broadcast the packet to everyone
Broadcast(memory, client)
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
Broadcast(memory, client);
}
}
catch (Exception e) {
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
client.Logger.Info($"Disconnected from the server: Connection reset");
}
else {
} else {
client.Logger.Error($"Disconnecting due to exception: {e}");
if (socket.Connected) {
#pragma warning disable CS4014
Task.Run(() => socket.DisconnectAsync(false))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
}
if (socket.Connected) Task.Run(() => socket.DisconnectAsync(false));
}
memory?.Dispose();
}
// client disconnected
if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) {
Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server");
}
else {
Logger.Info($"Client {remote} disconnected from the server");
}
disconnect:
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server");
bool wasConnected = client.Connected;
// Clients.Remove(client)
client.Connected = false;
try {
client.Dispose();
}
catch { /*lol*/ }
#pragma warning disable CS4014
if (wasConnected) {
Task.Run(() => Broadcast(new DisconnectPacket(), client))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
}
#pragma warning restore CS4014
}
private async Task ResendPackets(Client client) {
async Task trySendPack<T>(Client other, T? packet) where T : struct, IPacket {
if (packet == null) { return; }
try {
await client.Send((T) packet, other);
}
catch {
// lol who gives a fuck
}
};
async Task trySendMeta<T>(Client other, string packetType) where T : struct, IPacket {
if (!other.Metadata.ContainsKey(packetType)) { return; }
await trySendPack<T>(other, (T) other.Metadata[packetType]!);
};
await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => {
if (client.Id == other.Id) { return; }
await trySendMeta<CostumePacket>(other, "lastCostumePacket");
await trySendMeta<CapturePacket>(other, "lastCapturePacket");
await trySendPack<TagPacket>(other, other.GetTagPacket());
await trySendMeta<GamePacket>(other, "lastGamePacket");
await trySendMeta<PlayerPacket>(other, "lastPlayerPacket");
});
}
private async Task SendEmptyPackets(Client client, Client other) {
await other.Send(new TagPacket {
UpdateType = TagPacket.TagUpdate.State | TagPacket.TagUpdate.Time,
IsIt = false,
Seconds = 0,
Minutes = 0,
}, client);
await other.Send(new CapturePacket {
ModelName = "",
}, client);
Task.Run(() => Broadcast(new DisconnectPacket(), client));
}
private static PacketHeader GetHeader(Span<byte> data) {
@ -424,4 +313,4 @@ public class Server {
header.Deserialize(data[..Constants.HeaderSize]);
return header;
}
}
}

View File

@ -30,10 +30,10 @@ public class Settings {
LoadHandler?.Invoke();
}
public static void SaveSettings(bool silent = false) {
public static void SaveSettings() {
try {
File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy())));
if (!silent) { Logger.Info("Saved settings to settings.json"); }
Logger.Info("Saved settings to settings.json");
}
catch (Exception e) {
Logger.Error($"Failed to save settings.json {e}");
@ -43,9 +43,8 @@ public class Settings {
public ServerTable Server { get; set; } = new ServerTable();
public FlipTable Flip { get; set; } = new FlipTable();
public ScenarioTable Scenario { get; set; } = new ScenarioTable();
public BanListTable BanList { get; set; } = new BanListTable();
public BannedPlayers BanList { get; set; } = new BannedPlayers();
public DiscordTable Discord { get; set; } = new DiscordTable();
public ShineTable Shines { get; set; } = new ShineTable();
public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable();
public class ServerTable {
@ -58,34 +57,27 @@ public class Settings {
public bool MergeEnabled { get; set; } = false;
}
public class BanListTable {
public class BannedPlayers {
public bool Enabled { get; set; } = false;
public ISet<Guid> Players { get; set; } = new SortedSet<Guid>();
public ISet<string> IpAddresses { get; set; } = new SortedSet<string>();
public ISet<string> Stages { get; set; } = new SortedSet<string>();
public List<Guid> Players { get; set; } = new List<Guid>();
public List<string> IpAddresses { get; set; } = new List<string>();
}
public class FlipTable {
public bool Enabled { get; set; } = true;
public ISet<Guid> Players { get; set; } = new SortedSet<Guid>();
public List<Guid> Players { get; set; } = new List<Guid>();
public FlipOptions Pov { get; set; } = FlipOptions.Both;
}
public class DiscordTable {
public string? Token { get; set; }
public string Prefix { get; set; } = "$";
public string? CommandChannel { get; set; }
public string? LogChannel { get; set; }
}
public class ShineTable {
public bool Enabled { get; set; } = true;
public ISet<int> Excluded { get; set; } = new SortedSet<int> { 496 };
public class PersistShinesTable
{
public bool Enabled { get; set; } = false;
public string Filename { get; set; } = "./moons.json";
}
public class PersistShinesTable
{
public bool Enabled { get; set; } = false;
public string Filename { get; set; } = "./moons.json";
}
}
}

View File

@ -21,4 +21,24 @@ public static class Constants {
.ToDictionary(type => type.GetCustomAttribute<PacketAttribute>()!.Type, type => type);
public static int HeaderSize { get; } = PacketHeader.StaticSize;
}
public static readonly Dictionary<string, string> MapNames = new Dictionary<string, string>() {
{"cap", "CapWorldHomeStage"},
{"cascade", "WaterfallWorldHomeStage"},
{"sand", "SandWorldHomeStage"},
{"lake", "LakeWorldHomeStage"},
{"wooded", "ForestWorldHomeStage"},
{"cloud", "CloudWorldHomeStage"},
{"lost", "ClashWorldHomeStage"},
{"metro", "CityWorldHomeStage"},
{"sea", "SeaWorldHomeStage"},
{"snow", "SnowWorldHomeStage"},
{"lunch", "LavaWorldHomeStage"},
{"ruined", "BossRaidWorldHomeStage"},
{"bowser", "SkyWorldHomeStage"},
{"moon", "MoonWorldHomeStage"},
{"mush", "PeachWorldHomeStage"},
{"dark", "Special1WorldHomeStage"},
{"darker", "Special2WorldHomeStage"}
};
}

View File

@ -1,278 +0,0 @@
using System.Collections;
using System.Collections.Specialized;
namespace Shared;
public static class Stages {
public static string? Input2Stage(string input) {
// alias value
if (Alias2Stage.TryGetValue(input.ToLower(), out string? mapName)) {
return mapName;
}
// exact stage value
if (IsStage(input)) {
return input;
}
// force input value with a !
if (input.EndsWith("!")) {
return input.Substring(0, input.Length - 1);
}
return null;
}
public static string KingdomAliasMapping() {
string result = "";
foreach (DictionaryEntry item in Alias2Kingdom) {
result += item.Key + " -> " + item.Value + "\n";
}
return result;
}
public static bool IsAlias(string input) {
return Alias2Stage.ContainsKey(input);
}
public static bool IsStage(string input) {
return Stage2Alias.ContainsKey(input);
}
public static IEnumerable<string> StagesByInput(string input) {
if (IsAlias(input)) {
var stages = Stage2Alias
.Where(e => e.Value == input)
.Select(e => e.Key)
;
foreach (string stage in stages) {
yield return stage;
}
}
else {
string? stage = Input2Stage(input);
if (stage != null) {
yield return stage;
}
}
}
public static readonly Dictionary<string, string> Alias2Stage = new Dictionary<string, string>() {
{ "cap", "CapWorldHomeStage" },
{ "cascade", "WaterfallWorldHomeStage" },
{ "sand", "SandWorldHomeStage" },
{ "lake", "LakeWorldHomeStage" },
{ "wooded", "ForestWorldHomeStage" },
{ "cloud", "CloudWorldHomeStage" },
{ "lost", "ClashWorldHomeStage" },
{ "metro", "CityWorldHomeStage" },
{ "snow", "SnowWorldHomeStage" },
{ "sea", "SeaWorldHomeStage" },
{ "lunch", "LavaWorldHomeStage" },
{ "ruined", "BossRaidWorldHomeStage" },
{ "bowser", "SkyWorldHomeStage" },
{ "moon", "MoonWorldHomeStage" },
{ "mush", "PeachWorldHomeStage" },
{ "dark", "Special1WorldHomeStage" },
{ "darker", "Special2WorldHomeStage" },
{ "odyssey", "HomeShipInsideStage" },
};
public static readonly OrderedDictionary Alias2Kingdom = new OrderedDictionary() {
{ "cap", "Cap Kingdom" },
{ "cascade", "Cascade Kingdom" },
{ "sand", "Sand Kingdom" },
{ "lake", "Lake Kingdom" },
{ "wooded", "Wooded Kingdom" },
{ "cloud", "Cloud Kingdom" },
{ "lost", "Lost Kingdom" },
{ "metro", "Metro Kingdom" },
{ "snow", "Snow Kingdom" },
{ "sea", "Seaside Kingdom" },
{ "lunch", "Luncheon Kingdom" },
{ "ruined", "Ruined Kingdom" },
{ "bowser", "Bowser's Kingdom" },
{ "moon", "Moon Kingdom" },
{ "mush", "Mushroom Kingdom" },
{ "dark", "Dark Side" },
{ "darker", "Darker Side" },
{ "odyssey", "Odyssey" },
};
public static readonly Dictionary<string, string> Stage2Alias = new Dictionary<string, string>() {
{ "CapWorldHomeStage" , "cap" },
{ "CapWorldTowerStage" , "cap" },
{ "FrogSearchExStage" , "cap" },
{ "PoisonWaveExStage" , "cap" },
{ "PushBlockExStage" , "cap" },
{ "RollingExStage" , "cap" },
{ "WaterfallWorldHomeStage" , "cascade" },
{ "TrexPoppunExStage" , "cascade" },
{ "Lift2DExStage" , "cascade" },
{ "WanwanClashExStage" , "cascade" },
{ "CapAppearExStage" , "cascade" },
{ "WindBlowExStage" , "cascade" },
{ "SandWorldHomeStage" , "sand" },
{ "SandWorldShopStage" , "sand" },
{ "SandWorldSlotStage" , "sand" },
{ "SandWorldVibrationStage" , "sand" },
{ "SandWorldSecretStage" , "sand" },
{ "SandWorldMeganeExStage" , "sand" },
{ "SandWorldKillerExStage" , "sand" },
{ "SandWorldPressExStage" , "sand" },
{ "SandWorldSphinxExStage" , "sand" },
{ "SandWorldCostumeStage" , "sand" },
{ "SandWorldPyramid000Stage" , "sand" },
{ "SandWorldPyramid001Stage" , "sand" },
{ "SandWorldUnderground000Stage" , "sand" },
{ "SandWorldUnderground001Stage" , "sand" },
{ "SandWorldRotateExStage" , "sand" },
{ "MeganeLiftExStage" , "sand" },
{ "RocketFlowerExStage" , "sand" },
{ "WaterTubeExStage" , "sand" },
{ "LakeWorldHomeStage" , "lake" },
{ "LakeWorldShopStage" , "lake" },
{ "FastenerExStage" , "lake" },
{ "TrampolineWallCatchExStage" , "lake" },
{ "GotogotonExStage" , "lake" },
{ "FrogPoisonExStage" , "lake" },
{ "ForestWorldHomeStage" , "wooded" },
{ "ForestWorldWaterExStage" , "wooded" },
{ "ForestWorldTowerStage" , "wooded" },
{ "ForestWorldBossStage" , "wooded" },
{ "ForestWorldBonusStage" , "wooded" },
{ "ForestWorldCloudBonusExStage" , "wooded" },
{ "FogMountainExStage" , "wooded" },
{ "RailCollisionExStage" , "wooded" },
{ "ShootingElevatorExStage" , "wooded" },
{ "ForestWorldWoodsStage" , "wooded" },
{ "ForestWorldWoodsTreasureStage" , "wooded" },
{ "ForestWorldWoodsCostumeStage" , "wooded" },
{ "PackunPoisonExStage" , "wooded" },
{ "AnimalChaseExStage" , "wooded" },
{ "KillerRoadExStage" , "wooded" },
{ "CloudWorldHomeStage" , "cloud" },
{ "FukuwaraiKuriboStage" , "cloud" },
{ "Cube2DExStage" , "cloud" },
{ "ClashWorldHomeStage" , "lost" },
{ "ClashWorldShopStage" , "lost" },
{ "ImomuPoisonExStage" , "lost" },
{ "JangoExStage" , "lost" },
{ "CityWorldHomeStage" , "metro" },
{ "CityWorldMainTowerStage" , "metro" },
{ "CityWorldFactoryStage" , "metro" },
{ "CityWorldShop01Stage" , "metro" },
{ "CityWorldSandSlotStage" , "metro" },
{ "CityPeopleRoadStage" , "metro" },
{ "PoleGrabCeilExStage" , "metro" },
{ "TrexBikeExStage" , "metro" },
{ "PoleKillerExStage" , "metro" },
{ "Note2D3DRoomExStage" , "metro" },
{ "ShootingCityExStage" , "metro" },
{ "CapRotatePackunExStage" , "metro" },
{ "RadioControlExStage" , "metro" },
{ "ElectricWireExStage" , "metro" },
{ "Theater2DExStage" , "metro" },
{ "DonsukeExStage" , "metro" },
{ "SwingSteelExStage" , "metro" },
{ "BikeSteelExStage" , "metro" },
{ "SnowWorldHomeStage" , "snow" },
{ "SnowWorldTownStage" , "snow" },
{ "SnowWorldShopStage" , "snow" },
{ "SnowWorldLobby000Stage" , "snow" },
{ "SnowWorldLobby001Stage" , "snow" },
{ "SnowWorldRaceTutorialStage" , "snow" },
{ "SnowWorldRace000Stage" , "snow" },
{ "SnowWorldRace001Stage" , "snow" },
{ "SnowWorldCostumeStage" , "snow" },
{ "SnowWorldCloudBonusExStage" , "snow" },
{ "IceWalkerExStage" , "snow" },
{ "IceWaterBlockExStage" , "snow" },
{ "ByugoPuzzleExStage" , "snow" },
{ "IceWaterDashExStage" , "snow" },
{ "SnowWorldLobbyExStage" , "snow" },
{ "SnowWorldRaceExStage" , "snow" },
{ "SnowWorldRaceHardExStage" , "snow" },
{ "KillerRailCollisionExStage" , "snow" },
{ "SeaWorldHomeStage" , "sea" },
{ "SeaWorldUtsuboCaveStage" , "sea" },
{ "SeaWorldVibrationStage" , "sea" },
{ "SeaWorldSecretStage" , "sea" },
{ "SeaWorldCostumeStage" , "sea" },
{ "SeaWorldSneakingManStage" , "sea" },
{ "SenobiTowerExStage" , "sea" },
{ "CloudExStage" , "sea" },
{ "WaterValleyExStage" , "sea" },
{ "ReflectBombExStage" , "sea" },
{ "TogezoRotateExStage" , "sea" },
{ "LavaWorldHomeStage" , "lunch" },
{ "LavaWorldUpDownExStage" , "lunch" },
{ "LavaBonus1Zone" , "lunch" },
{ "LavaWorldShopStage" , "lunch" },
{ "LavaWorldCostumeStage" , "lunch" },
{ "ForkExStage" , "lunch" },
{ "LavaWorldExcavationExStage" , "lunch" },
{ "LavaWorldClockExStage" , "lunch" },
{ "LavaWorldBubbleLaneExStage" , "lunch" },
{ "LavaWorldTreasureStage" , "lunch" },
{ "GabuzouClockExStage" , "lunch" },
{ "CapAppearLavaLiftExStage" , "lunch" },
{ "LavaWorldFenceLiftExStage" , "lunch" },
{ "BossRaidWorldHomeStage" , "ruined" },
{ "DotTowerExStage" , "ruined" },
{ "BullRunExStage" , "ruined" },
{ "SkyWorldHomeStage" , "bowser" },
{ "SkyWorldShopStage" , "bowser" },
{ "SkyWorldCostumeStage" , "bowser" },
{ "SkyWorldCloudBonusExStage" , "bowser" },
{ "SkyWorldTreasureStage" , "bowser" },
{ "JizoSwitchExStage" , "bowser" },
{ "TsukkunRotateExStage" , "bowser" },
{ "KaronWingTowerStage" , "bowser" },
{ "TsukkunClimbExStage" , "bowser" },
{ "MoonWorldHomeStage" , "moon" },
{ "MoonWorldCaptureParadeStage" , "moon" },
{ "MoonWorldWeddingRoomStage" , "moon" },
{ "MoonWorldKoopa1Stage" , "moon" },
{ "MoonWorldBasementStage" , "moon" },
{ "MoonWorldWeddingRoom2Stage" , "moon" },
{ "MoonWorldKoopa2Stage" , "moon" },
{ "MoonWorldShopRoom" , "moon" },
{ "MoonWorldSphinxRoom" , "moon" },
{ "MoonAthleticExStage" , "moon" },
{ "Galaxy2DExStage" , "moon" },
{ "PeachWorldHomeStage" , "mush" },
{ "PeachWorldShopStage" , "mush" },
{ "PeachWorldCastleStage" , "mush" },
{ "PeachWorldCostumeStage" , "mush" },
{ "FukuwaraiMarioStage" , "mush" },
{ "DotHardExStage" , "mush" },
{ "YoshiCloudExStage" , "mush" },
{ "PeachWorldPictureBossMagmaStage" , "mush" },
{ "RevengeBossMagmaStage" , "mush" },
{ "PeachWorldPictureGiantWanderBossStage" , "mush" },
{ "RevengeGiantWanderBossStage" , "mush" },
{ "PeachWorldPictureBossKnuckleStage" , "mush" },
{ "RevengeBossKnuckleStage" , "mush" },
{ "PeachWorldPictureBossForestStage" , "mush" },
{ "RevengeForestBossStage" , "mush" },
{ "PeachWorldPictureMofumofuStage" , "mush" },
{ "RevengeMofumofuStage" , "mush" },
{ "PeachWorldPictureBossRaidStage" , "mush" },
{ "RevengeBossRaidStage" , "mush" },
{ "Special1WorldHomeStage" , "dark" },
{ "Special1WorldTowerStackerStage" , "dark" },
{ "Special1WorldTowerBombTailStage" , "dark" },
{ "Special1WorldTowerFireBlowerStage" , "dark" },
{ "Special1WorldTowerCapThrowerStage" , "dark" },
{ "KillerRoadNoCapExStage" , "dark" },
{ "PackunPoisonNoCapExStage" , "dark" },
{ "BikeSteelNoCapExStage" , "dark" },
{ "ShootingCityYoshiExStage" , "dark" },
{ "SenobiTowerYoshiExStage" , "dark" },
{ "LavaWorldUpDownYoshiExStage" , "dark" },
{ "Special2WorldHomeStage" , "darker" },
{ "Special2WorldLavaStage" , "darker" },
{ "Special2WorldCloudStage" , "darker" },
{ "Special2WorldKoopaStage" , "darker" },
{ "HomeShipInsideStage" , "odyssey" },
};
}

View File

@ -21,7 +21,7 @@ PacketType[] reboundPackets = {
// PacketType.Shine
};
//string lastCapture = ""; //not referenced
string lastCapture = "";
List<TcpClient> clients = new List<TcpClient>();
async Task S(string n, Guid otherId, Guid ownId) {
@ -77,15 +77,13 @@ async Task S(string n, Guid otherId, Guid ownId) {
continue;
}
if (type == PacketType.Player) {
#pragma warning disable CS4014
Task.Run(async () => {
await Task.Delay(1000);
header.Id = ownId;
MemoryMarshal.Write(owner.Memory.Span[..Constants.HeaderSize], ref header);
await stream.WriteAsync(owner.Memory[..(Constants.HeaderSize + header.PacketSize)]);
owner.Dispose();
}).ContinueWith(x => { if (x.Exception != null) { logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
});
continue;
}
header.Id = ownId;

View File

@ -1,51 +0,0 @@
#!/bin/bash
set -euo pipefail
if [[ "$#" == "0" ]] || [[ "$#" > "1" ]] || ! [[ "$1" =~ ^(all|x64|arm|arm64|win64)$ ]] ; then
echo "Usage: docker-build.sh {all|x64|arm|arm64|win64}"
exit 1
fi
DIR=$(dirname "$(realpath $0)")
cd "$DIR"
declare -A archs=(
["x64"]="linux-x64"
["arm"]="linux-arm"
["arm64"]="linux-arm64"
["win64"]="win-x64"
)
for sub in "${!archs[@]}" ; do
arch="${archs[$sub]}"
if [[ "$1" != "all" ]] && [[ "$1" != "$sub" ]] ; then
continue
fi
docker run \
-u `id -u`:`id -g` \
-v "/$DIR/"://app/ \
-w //app/ \
-e DOTNET_CLI_HOME=//app/cache/ \
-e XDG_DATA_HOME=//app/cache/ \
mcr.microsoft.com/dotnet/sdk:6.0 \
dotnet publish \
./Server/Server.csproj \
-r $arch \
-c Release \
-o /app/bin/$sub/ \
--self-contained \
-p:publishSingleFile=true \
;
filename="Server"
ext=""
if [[ "$sub" == "arm" ]] ; then filename="Server.arm";
elif [[ "$sub" == "arm64" ]] ; then filename="Server.arm64";
elif [[ "$sub" == "win64" ]] ; then filename="Server.exe"; ext=".exe";
fi
mv ./bin/$sub/Server$ext ./bin/$filename
rm -rf ./bin/$sub/
done

View File

@ -6,7 +6,6 @@ services:
#build: .
#user: 1000:1000
stdin_open: true
restart: unless-stopped
ports:
- 1027:1027
volumes:

View File

@ -14,11 +14,6 @@ WorkingDirectory=/home/user/SMOServer/
Restart=always
# amount of time to wait before restarting the service
RestartSec=5
#TTY Input
StandardInput=tty-force
TTYVHangup=yes
TTYPath=/dev/tty20
TTYReset=yes
[Install]
WantedBy=multi-user.target