Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
Robin C. Ladiges | 497b5b44d6 | |
Robin C. Ladiges | 4de654b6e4 | |
Robin C. Ladiges | 082e480b1e | |
Robin C. Ladiges | 9511d07f09 | |
Robin C. Ladiges | dc20a9c831 | |
Robin C. Ladiges | 61e6fcf2a3 | |
Robin C. Ladiges | 20ee74d0d6 | |
Robin C. Ladiges | dd0de0da78 | |
Robin C. Ladiges | d6a8df448c | |
Robin C. Ladiges | 86c79177fd | |
Robin C. Ladiges | 1e9d334d6f | |
Robin C. Ladiges | 71bb96bf1e | |
Robin C. Ladiges | 47fc1527bf | |
Robin C. Ladiges | a0642e6a30 | |
Robin C. Ladiges | f0d837190a | |
Robin C. Ladiges | 122a3cd80d | |
Robin C. Ladiges | 69cef89953 | |
Robin C. Ladiges | 6285abfc4e | |
Robin C. Ladiges | 472c8856bc | |
Robin C. Ladiges | 53442b598e | |
Robin C. Ladiges | 92d4bdd195 | |
Robin C. Ladiges | c41499f953 | |
Robin C. Ladiges | 76fc4a80a6 |
|
@ -0,0 +1,4 @@
|
|||
/Server/bin/
|
||||
/Server/obj/
|
||||
/Shared/bin/
|
||||
/Shared/obj/
|
|
@ -73,3 +73,35 @@ 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
|
||||
|
|
|
@ -6,3 +6,6 @@ riderModule.iml
|
|||
.idea/
|
||||
settings.json
|
||||
.vs/
|
||||
|
||||
/cache/
|
||||
/data/
|
||||
|
|
|
@ -5,8 +5,8 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 as build
|
|||
|
||||
WORKDIR /app/
|
||||
|
||||
COPY ./Server/ ./Server/
|
||||
COPY ./Shared/ ./Shared/
|
||||
COPY ./Shared/Shared.csproj ./Shared/Shared.csproj
|
||||
COPY ./Server/Server.csproj ./Server/Server.csproj
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
|
@ -16,6 +16,9 @@ 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 \
|
||||
|
|
|
@ -0,0 +1,359 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ 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;
|
||||
|
@ -28,9 +30,21 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -39,9 +53,14 @@ 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);
|
||||
}
|
||||
|
@ -57,14 +76,42 @@ public class Client : IDisposable {
|
|||
public async Task Send(Memory<byte> data, Client? sender) {
|
||||
PacketHeader header = new PacketHeader();
|
||||
header.Deserialize(data.Span);
|
||||
if (!Connected && header.Type is not PacketType.Connect) {
|
||||
|
||||
if (!Connected && !Ignored && header.Type != 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;
|
||||
}
|
||||
|
@ -83,4 +130,4 @@ public class Client : IDisposable {
|
|||
public override int GetHashCode() {
|
||||
return Id.GetHashCode(); //relies upon same info as == operator.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,11 @@ public class DiscordBot {
|
|||
Task.Run(Reconnect);
|
||||
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");
|
||||
if (Config.Token == null) return;
|
||||
Settings.LoadHandler += SettingsLoadHandler;
|
||||
}
|
||||
|
||||
|
@ -39,19 +39,31 @@ public class DiscordBot {
|
|||
}
|
||||
|
||||
private async void SettingsLoadHandler() {
|
||||
try {
|
||||
if (DiscordClient == null || Token != Config.Token)
|
||||
await Run();
|
||||
//CommandChannel not currently used
|
||||
if (Config.CommandChannel != null)
|
||||
CommandChannel = await (DiscordClient?.GetChannelAsync(ulong.Parse(Config.CommandChannel)) ??
|
||||
throw new NullReferenceException("Discord client not setup yet!"));
|
||||
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.CommandChannel}\"");
|
||||
Logger.Error(e);
|
||||
if (DiscordClient == null || Token != Config.Token) {
|
||||
await Run();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,4 +157,4 @@ public class DiscordBot {
|
|||
Logger.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ 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();
|
||||
|
@ -63,23 +62,11 @@ 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) {
|
||||
|
@ -87,11 +74,12 @@ async Task ClientSyncShineBag(Client client) {
|
|||
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).ToArray()) {
|
||||
clientBag.Add(shine);
|
||||
foreach (int shine in shineBag.Except(clientBag).Except(Settings.Instance.Shines.Excluded).ToArray()) {
|
||||
if (!client.Connected) return;
|
||||
await client.Send(new ShinePacket {
|
||||
ShineId = shine
|
||||
});
|
||||
clientBag.Add(shine);
|
||||
}
|
||||
} catch {
|
||||
// errors that can happen when sending will crash the server :)
|
||||
|
@ -101,7 +89,7 @@ async Task ClientSyncShineBag(Client client) {
|
|||
async void SyncShineBag() {
|
||||
try {
|
||||
await PersistShines();
|
||||
await Parallel.ForEachAsync(server.Clients.ToArray(), async (client, _) => await ClientSyncShineBag(client));
|
||||
await Parallel.ForEachAsync(server.ClientsConnected.ToArray(), async (client, _) => await ClientSyncShineBag(client));
|
||||
} catch {
|
||||
// errors that can happen shines change will crash the server :)
|
||||
}
|
||||
|
@ -115,13 +103,50 @@ 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;
|
||||
|
@ -145,8 +170,7 @@ server.PacketHandler = (c, p) => {
|
|||
server.BroadcastReplace(gamePacket, c, (from, to, gp) => {
|
||||
gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200;
|
||||
#pragma warning disable CS4014
|
||||
to.Send(gp, from)
|
||||
.ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
|
||||
to.Send(gp, from).ContinueWith(logError);
|
||||
#pragma warning restore CS4014
|
||||
});
|
||||
return false;
|
||||
|
@ -154,19 +178,43 @@ server.PacketHandler = (c, p) => {
|
|||
|
||||
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 CostumePacket:
|
||||
|
||||
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
|
||||
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"]!;
|
||||
shineBag.Add(shinePacket.ShineId);
|
||||
|
@ -176,75 +224,89 @@ 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));
|
||||
|
||||
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(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
|
||||
server.Broadcast(playerPacket, c).ContinueWith(logError);
|
||||
#pragma warning restore CS4014
|
||||
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));
|
||||
}
|
||||
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(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
|
||||
to.Send(sp, from).ContinueWith(logError);
|
||||
#pragma warning restore CS4014
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true; // Broadcast packet to all other clients
|
||||
};
|
||||
|
||||
(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] == "*")
|
||||
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)));
|
||||
if (!search.Any())
|
||||
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] == "!*")
|
||||
if (args[0] == "!*") {
|
||||
toActUpon.Remove(exact);
|
||||
else
|
||||
}
|
||||
else {
|
||||
toActUpon.Add(exact);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!ambig.Any(x => x.arg == arg))
|
||||
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
|
||||
}
|
||||
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] == "!*")
|
||||
if (args[0] == "!*") {
|
||||
toActUpon.Remove(search.First());
|
||||
else
|
||||
}
|
||||
else {
|
||||
toActUpon.Add(search.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,71 +353,28 @@ CommandHandler.RegisterCommand("crash", args => {
|
|||
}
|
||||
|
||||
foreach (Client user in res.toActUpon) {
|
||||
Task.Run(async () => {
|
||||
await user.Send(new ChangeStagePacket {
|
||||
Id = "$among$us/SubArea",
|
||||
Stage = "$agogusStage",
|
||||
Scenario = 21,
|
||||
SubScenarioType = 69 // invalid id
|
||||
});
|
||||
user.Dispose();
|
||||
});
|
||||
BanLists.Crash(user);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
});
|
||||
|
||||
CommandHandler.RegisterCommand("ban", args => {
|
||||
if (args.Length == 0) {
|
||||
return "Usage: ban <* | !* (usernames to not ban...) | (usernames to ban...)>";
|
||||
}
|
||||
|
||||
var res = MultiUserCommandHelper(args);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append(res.toActUpon.Count > 0 ? "Banned: " + 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) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
Settings.SaveSettings();
|
||||
return sb.ToString();
|
||||
});
|
||||
CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); });
|
||||
CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); });
|
||||
|
||||
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] == "*"
|
||||
|
@ -377,17 +396,13 @@ 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;
|
||||
}
|
||||
|
||||
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```";
|
||||
string? stage = Stages.Input2Stage(args[0]);
|
||||
if (stage == null) {
|
||||
return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```";
|
||||
}
|
||||
|
||||
Client[] players = server.Clients.Where(c => c.Connected).ToArray();
|
||||
|
@ -562,12 +577,16 @@ CommandHandler.RegisterCommand("flip", args => {
|
|||
});
|
||||
|
||||
CommandHandler.RegisterCommand("shine", args => {
|
||||
const string optionUsage = "Valid options: list, clear, sync, send, set";
|
||||
const string optionUsage = "Valid options: list, clear, sync, send, set, include, exclude";
|
||||
if (args.Length < 1)
|
||||
return optionUsage;
|
||||
switch (args[0]) {
|
||||
case "list" when args.Length == 1:
|
||||
return $"Shines: {string.Join(", ", shineBag)}";
|
||||
return $"Shines: {string.Join(", ", shineBag)}" + (
|
||||
Settings.Instance.Shines.Excluded.Count() > 0
|
||||
? "\nExcluded Shines: " + string.Join(", ", Settings.Instance.Shines.Excluded)
|
||||
: ""
|
||||
);
|
||||
case "clear" when args.Length == 1:
|
||||
shineBag.Clear();
|
||||
Task.Run(async () => {
|
||||
|
@ -604,6 +623,21 @@ CommandHandler.RegisterCommand("shine", args => {
|
|||
|
||||
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;
|
||||
}
|
||||
|
@ -651,10 +685,11 @@ Task.Run(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}).ContinueWith(x => { if (x.Exception != null) { consoleLogger.Error(x.Exception.ToString()); } });
|
||||
}).ContinueWith(logError);
|
||||
#pragma warning restore CS4014
|
||||
|
||||
await listenTask;
|
||||
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()
|
||||
{
|
||||
|
@ -666,4 +701,4 @@ if (restartRequested) //need to do this here because this needs to happen after
|
|||
}
|
||||
else
|
||||
consoleLogger.Info(unableToStartMsg);
|
||||
}
|
||||
}
|
||||
|
|
249
Server/Server.cs
249
Server/Server.cs
|
@ -31,6 +31,7 @@ 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))
|
||||
|
@ -64,7 +65,7 @@ public class Server {
|
|||
|
||||
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..]);
|
||||
}
|
||||
|
@ -73,27 +74,29 @@ 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(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet);
|
||||
foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.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).Select(async client => {
|
||||
return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).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);
|
||||
|
@ -107,7 +110,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 != sender).Select(client => client.Send(data.Memory, sender)));
|
||||
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender)));
|
||||
data.Dispose();
|
||||
}
|
||||
|
||||
|
@ -117,7 +120,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 != sender).Select(client => client.Send(data, sender)));
|
||||
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender)));
|
||||
}
|
||||
|
||||
public Client? FindExistingClient(Guid id) {
|
||||
|
@ -127,10 +130,9 @@ 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) {
|
||||
|
@ -142,7 +144,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 {socket.RemoteEndPoint} disconnected.");
|
||||
Logger.Info($"Socket {remote} disconnected.");
|
||||
if (socket.Connected) await socket.DisconnectAsync(false);
|
||||
return false;
|
||||
}
|
||||
|
@ -153,8 +155,9 @@ 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) {
|
||||
|
@ -162,67 +165,101 @@ 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;
|
||||
if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
|
||||
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})");
|
||||
}
|
||||
|
||||
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) {
|
||||
if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) {
|
||||
client.Logger.Error($"Turned away as server is at max 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;
|
||||
memory.Dispose();
|
||||
goto disconnect;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool firstConn = false;
|
||||
// detect and handle reconnections
|
||||
bool isClientNew = true;
|
||||
switch (connect.ConnectionType) {
|
||||
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;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ConnectPacket.ConnectionTypes.FirstConnection:
|
||||
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();
|
||||
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();
|
||||
}
|
||||
newClient.Socket = client.Socket;
|
||||
client = newClient;
|
||||
} else {
|
||||
firstConn = true;
|
||||
}
|
||||
else {
|
||||
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Exception($"Invalid connection type {connect.ConnectionType}");
|
||||
default: {
|
||||
throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})");
|
||||
}
|
||||
}
|
||||
|
||||
client.Name = connect.ClientName;
|
||||
client.Connected = true;
|
||||
if (firstConn) {
|
||||
// do any cleanup required when it comes to new clients
|
||||
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;
|
||||
if (isClientNew) {
|
||||
// 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);
|
||||
|
||||
Clients.Add(client);
|
||||
|
||||
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
|
||||
|
@ -230,26 +267,35 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
|
||||
// for all other clients that are already connected
|
||||
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != client.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)]);
|
||||
|
@ -257,25 +303,30 @@ 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}/{socket.RemoteEndPoint}) connected.");
|
||||
} else if (header.Id != client.Id && client.Id != Guid.Empty) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
@ -283,7 +334,9 @@ public class Server {
|
|||
catch (Exception e) {
|
||||
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
|
||||
|
@ -292,7 +345,8 @@ public class Server {
|
|||
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
|
||||
|
@ -305,10 +359,15 @@ public class Server {
|
|||
memory?.Dispose();
|
||||
}
|
||||
|
||||
disconnect:
|
||||
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server");
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Clients.Remove(client)
|
||||
bool wasConnected = client.Connected;
|
||||
client.Connected = false;
|
||||
try {
|
||||
client.Dispose();
|
||||
|
@ -316,11 +375,49 @@ public class Server {
|
|||
catch { /*lol*/ }
|
||||
|
||||
#pragma warning disable CS4014
|
||||
Task.Run(() => Broadcast(new DisconnectPacket(), client))
|
||||
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
|
||||
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);
|
||||
}
|
||||
|
||||
private static PacketHeader GetHeader(Span<byte> data) {
|
||||
//no need to error check, the client will disconnect when the packet is invalid :)
|
||||
PacketHeader header = new PacketHeader();
|
||||
|
|
|
@ -30,10 +30,10 @@ public class Settings {
|
|||
LoadHandler?.Invoke();
|
||||
}
|
||||
|
||||
public static void SaveSettings() {
|
||||
public static void SaveSettings(bool silent = false) {
|
||||
try {
|
||||
File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy())));
|
||||
Logger.Info("Saved settings to settings.json");
|
||||
if (!silent) { Logger.Info("Saved settings to settings.json"); }
|
||||
}
|
||||
catch (Exception e) {
|
||||
Logger.Error($"Failed to save settings.json {e}");
|
||||
|
@ -43,7 +43,7 @@ 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 BannedPlayers BanList { get; set; } = new BannedPlayers();
|
||||
public BanListTable BanList { get; set; } = new BanListTable();
|
||||
public DiscordTable Discord { get; set; } = new DiscordTable();
|
||||
public ShineTable Shines { get; set; } = new ShineTable();
|
||||
public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable();
|
||||
|
@ -58,15 +58,16 @@ public class Settings {
|
|||
public bool MergeEnabled { get; set; } = false;
|
||||
}
|
||||
|
||||
public class BannedPlayers {
|
||||
public class BanListTable {
|
||||
public bool Enabled { get; set; } = false;
|
||||
public List<Guid> Players { get; set; } = new List<Guid>();
|
||||
public List<string> IpAddresses { get; set; } = new List<string>();
|
||||
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 class FlipTable {
|
||||
public bool Enabled { get; set; } = true;
|
||||
public List<Guid> Players { get; set; } = new List<Guid>();
|
||||
public ISet<Guid> Players { get; set; } = new SortedSet<Guid>();
|
||||
public FlipOptions Pov { get; set; } = FlipOptions.Both;
|
||||
}
|
||||
|
||||
|
@ -79,6 +80,7 @@ public class Settings {
|
|||
|
||||
public class ShineTable {
|
||||
public bool Enabled { get; set; } = true;
|
||||
public ISet<int> Excluded { get; set; } = new SortedSet<int> { 496 };
|
||||
}
|
||||
|
||||
public class PersistShinesTable
|
||||
|
@ -86,4 +88,4 @@ public class Settings {
|
|||
public bool Enabled { get; set; } = false;
|
||||
public string Filename { get; set; } = "./moons.json";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,24 +21,4 @@ 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"}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
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" },
|
||||
};
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
#!/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
|
|
@ -6,6 +6,7 @@ services:
|
|||
#build: .
|
||||
#user: 1000:1000
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 1027:1027
|
||||
volumes:
|
||||
|
|
Loading…
Reference in New Issue