Compare commits
66 Commits
Author | SHA1 | Date |
---|---|---|
Aubrey | 6ec213b4be | |
Robin C. Ladiges | 3affb59eb9 | |
Robin C. Ladiges | 7a055be9cf | |
Robin C. Ladiges | d69d9b8296 | |
Robin C. Ladiges | fb55820e2b | |
Robin C. Ladiges | bec6dde076 | |
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 | |
Jack Baron | e14616030c | |
Jack Baron | 24a34c35a1 | |
TheUbMunster | 783b876e09 | |
TheUbMunster | 4d743b3e1b | |
TheUbMunster | 67a740dec9 | |
TheUbMunster | 4c4ce35f14 | |
TheUbMunster | 86fb18962f | |
TheUbMunster | 06e3b5d40a | |
TheUbMunster | 02f936816c | |
TheUbMunster | 82e94334c9 | |
TheUbMunster | b6e634ada4 | |
TheUbMunster | d828a704c1 | |
TheUbMunster | 08d1020770 | |
Sanae | 9c01f30c5e | |
Piplup | cee8bd580f | |
Piplup | fa1db28037 | |
raym55 | c86be55717 | |
Piplup | 31361d54ca | |
Piplup | 427acbcf88 | |
Piplup | 75eea98d8e | |
Sanae | e7a3347a37 | |
Sanae | fcea1d898e | |
TheUbMunster | 8baf75155e | |
TheUbMunster | fc2f9b1417 | |
TheUbMunster | 33fdd69e15 | |
TheUbMunster | 5af8001398 | |
TheUbMunster | ba02c88b13 | |
TheUbMunster | 3b8ba17217 | |
TheUbMunster | 95c918b5c4 | |
TheUbMunster | 8350133d49 | |
TheUbMunster | d3b8ede229 | |
TheUbMunster | 216db7ba15 | |
TheUbMunster | 3a8e7ffb93 | |
TheUbMunster | 12c84792e8 | |
TheUbMunster | d2c8c8d3cd | |
TheUbMunster | 068cc7c06d | |
Sanae | 66114bdecb |
|
@ -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
|
platforms : linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
cache-from : type=gha,scope=${{ github.workflow }}
|
cache-from : type=gha,scope=${{ github.workflow }}
|
||||||
cache-to : type=gha,scope=${{ github.workflow }},mode=max
|
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/
|
.idea/
|
||||||
settings.json
|
settings.json
|
||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
|
/cache/
|
||||||
|
/data/
|
||||||
|
|
|
@ -5,8 +5,8 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 as build
|
||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
COPY ./Server/ ./Server/
|
COPY ./Shared/Shared.csproj ./Shared/Shared.csproj
|
||||||
COPY ./Shared/ ./Shared/
|
COPY ./Server/Server.csproj ./Server/Server.csproj
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
@ -16,6 +16,9 @@ RUN dotnet restore \
|
||||||
-r debian.11-`echo $TARGETARCH | sed 's@^amd@x@'` \
|
-r debian.11-`echo $TARGETARCH | sed 's@^amd@x@'` \
|
||||||
;
|
;
|
||||||
|
|
||||||
|
COPY ./Shared/ ./Shared/
|
||||||
|
COPY ./Server/ ./Server/
|
||||||
|
|
||||||
# Build application binary
|
# Build application binary
|
||||||
RUN dotnet publish \
|
RUN dotnet publish \
|
||||||
./Server/Server.csproj \
|
./Server/Server.csproj \
|
||||||
|
|
|
@ -29,6 +29,7 @@ cp smo.serivce /etc/systemd/system/smo.service
|
||||||
# edit ExecStart to your path for the server executable and change WorkingDirectory to the server directory
|
# edit ExecStart to your path for the server executable and change WorkingDirectory to the server directory
|
||||||
chmod +x filepath to the server executable
|
chmod +x filepath to the server executable
|
||||||
systemctl enable --now smo.service
|
systemctl enable --now smo.service
|
||||||
|
# for TTY access i would recommand conspy but there is also reptyr, chvt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run docker image
|
## Run docker image
|
||||||
|
@ -78,8 +79,10 @@ Maxplayers: the max amount of players that can join, default: 8
|
||||||
Flip: flips the player upside down, defaults: enabled: true, pov: both
|
Flip: flips the player upside down, defaults: enabled: true, pov: both
|
||||||
Scenario: sync's scenario's for all players on the server, default: false
|
Scenario: sync's scenario's for all players on the server, default: false
|
||||||
Banlist: banned people are unable to join 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
|
### 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
|
Token: the token of the bot you want to load into, default: null
|
||||||
Prefix: the bot prefix to be used, default: $
|
Prefix: the bot prefix to be used, default: $
|
||||||
LogChannel: logs the server console to that channel, default: null
|
LogChannel: logs the server console to that channel, default: null
|
|
@ -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 class Client : IDisposable {
|
||||||
public readonly ConcurrentDictionary<string, object?> Metadata = new ConcurrentDictionary<string, object?>(); // can be used to store any information about a player
|
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 Connected = false;
|
||||||
|
public bool Ignored = false;
|
||||||
|
public bool Banned = false;
|
||||||
public CostumePacket? CurrentCostume = null; // required for proper client sync
|
public CostumePacket? CurrentCostume = null; // required for proper client sync
|
||||||
public string Name {
|
public string Name {
|
||||||
get => Logger.Name;
|
get => Logger.Name;
|
||||||
|
@ -20,7 +22,7 @@ public class Client : IDisposable {
|
||||||
|
|
||||||
public Guid Id;
|
public Guid Id;
|
||||||
public Socket? Socket;
|
public Socket? Socket;
|
||||||
public Server Server { get; init; }
|
public Server Server { get; init; } = null!; //init'd in object initializer
|
||||||
public Logger Logger { get; }
|
public Logger Logger { get; }
|
||||||
|
|
||||||
public Client(Socket socket) {
|
public Client(Socket socket) {
|
||||||
|
@ -28,10 +30,22 @@ public class Client : IDisposable {
|
||||||
Logger = new Logger("Unknown User");
|
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() {
|
public void Dispose() {
|
||||||
if (Socket?.Connected is true)
|
if (Socket?.Connected is true) {
|
||||||
Socket.Disconnect(false);
|
Socket.Disconnect(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task Send<T>(T packet, Client? sender = null) where T : struct, IPacket {
|
public async Task Send<T>(T packet, Client? sender = null) where T : struct, IPacket {
|
||||||
|
@ -39,6 +53,11 @@ public class Client : IDisposable {
|
||||||
|
|
||||||
PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)];
|
PacketAttribute packetAttribute = Constants.PacketMap[typeof(T)];
|
||||||
try {
|
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 {
|
Server.FillPacket(new PacketHeader {
|
||||||
Id = sender?.Id ?? Id,
|
Id = sender?.Id ?? Id,
|
||||||
Type = packetAttribute.Type,
|
Type = packetAttribute.Type,
|
||||||
|
@ -57,14 +76,42 @@ public class Client : IDisposable {
|
||||||
public async Task Send(Memory<byte> data, Client? sender) {
|
public async Task Send(Memory<byte> data, Client? sender) {
|
||||||
PacketHeader header = new PacketHeader();
|
PacketHeader header = new PacketHeader();
|
||||||
header.Deserialize(data.Span);
|
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");
|
Server.Logger.Error($"Didn't send {header.Type} to {Id} because they weren't connected yet");
|
||||||
return;
|
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);
|
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) {
|
public static bool operator ==(Client? left, Client? right) {
|
||||||
return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id;
|
return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id;
|
||||||
}
|
}
|
||||||
|
@ -72,4 +119,15 @@ public class Client : IDisposable {
|
||||||
public static bool operator !=(Client? left, Client? right) {
|
public static bool operator !=(Client? left, Client? right) {
|
||||||
return !(left == 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.
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -11,6 +11,7 @@ public class DiscordBot {
|
||||||
private Settings.DiscordTable Config => Settings.Instance.Discord;
|
private Settings.DiscordTable Config => Settings.Instance.Discord;
|
||||||
private string Prefix => Config.Prefix;
|
private string Prefix => Config.Prefix;
|
||||||
private readonly Logger Logger = new Logger("Discord");
|
private readonly Logger Logger = new Logger("Discord");
|
||||||
|
private DiscordChannel? CommandChannel;
|
||||||
private DiscordChannel? LogChannel;
|
private DiscordChannel? LogChannel;
|
||||||
private bool Reconnecting;
|
private bool Reconnecting;
|
||||||
|
|
||||||
|
@ -24,6 +25,10 @@ public class DiscordBot {
|
||||||
return "Restarting Discord bot";
|
return "Restarting Discord bot";
|
||||||
});
|
});
|
||||||
if (Config.Token == null) return;
|
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;
|
Settings.LoadHandler += SettingsLoadHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,23 +39,49 @@ public class DiscordBot {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SettingsLoadHandler() {
|
private async void SettingsLoadHandler() {
|
||||||
try {
|
if (DiscordClient == null || Token != Config.Token) {
|
||||||
if (DiscordClient == null || Token != Config.Token)
|
|
||||||
await Run();
|
await Run();
|
||||||
if (Config.LogChannel != null)
|
}
|
||||||
LogChannel = await (DiscordClient?.GetChannelAsync(ulong.Parse(Config.LogChannel)) ??
|
|
||||||
throw new NullReferenceException("Discord client not setup yet!"));
|
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) {
|
} catch (Exception e) {
|
||||||
Logger.Error($"Failed to get log channel \"{Config.LogChannel}\"");
|
Logger.Error($"Failed to get log channel \"{Config.LogChannel}\"");
|
||||||
Logger.Error(e);
|
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 _) {
|
private async void Log(string source, string level, string text, ConsoleColor _) {
|
||||||
try {
|
try {
|
||||||
if (DiscordClient != null && LogChannel != null) {
|
if (DiscordClient != null && LogChannel != null) {
|
||||||
await DiscordClient.SendMessageAsync(LogChannel,
|
foreach (string mesg in SplitMessage(Logger.PrefixNewLines(text, $"{level} [{source}]"), 1994)) //room for 6 '`'
|
||||||
$"```{Logger.PrefixNewLines(text, $"{level} [{source}]")}```");
|
await DiscordClient.SendMessageAsync(LogChannel, $"```{mesg}```");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// don't log again, it'll just stack overflow the server!
|
// don't log again, it'll just stack overflow the server!
|
||||||
|
@ -78,23 +109,34 @@ public class DiscordBot {
|
||||||
Logger.Info(
|
Logger.Info(
|
||||||
$"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}");
|
$"Discord bot logged in as {DiscordClient.CurrentUser.Username}#{DiscordClient.CurrentUser.Discriminator}");
|
||||||
Reconnecting = false;
|
Reconnecting = false;
|
||||||
string mentionPrefix = $"{DiscordClient.CurrentUser.Mention} ";
|
string mentionPrefix = $"{DiscordClient.CurrentUser.Mention}";
|
||||||
DiscordClient.MessageCreated += async (_, args) => {
|
DiscordClient.MessageCreated += async (_, args) => {
|
||||||
if (args.Author.IsCurrent) return;
|
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
|
||||||
try {
|
try {
|
||||||
DiscordMessage msg = args.Message;
|
DiscordMessage msg = args.Message;
|
||||||
|
string? resp = null;
|
||||||
if (string.IsNullOrEmpty(Prefix)) {
|
if (string.IsNullOrEmpty(Prefix)) {
|
||||||
await msg.Channel.TriggerTypingAsync();
|
await msg.Channel.TriggerTypingAsync();
|
||||||
await msg.RespondAsync(string.Join('\n',
|
resp = string.Join('\n', CommandHandler.GetResult(msg.Content).ReturnStrings);
|
||||||
CommandHandler.GetResult(msg.Content).ReturnStrings));
|
|
||||||
} else if (msg.Content.StartsWith(Prefix)) {
|
} else if (msg.Content.StartsWith(Prefix)) {
|
||||||
await msg.Channel.TriggerTypingAsync();
|
await msg.Channel.TriggerTypingAsync();
|
||||||
await msg.RespondAsync(string.Join('\n',
|
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings);
|
||||||
CommandHandler.GetResult(msg.Content[Prefix.Length..]).ReturnStrings));
|
|
||||||
} else if (msg.Content.StartsWith(mentionPrefix)) {
|
} else if (msg.Content.StartsWith(mentionPrefix)) {
|
||||||
await msg.Channel.TriggerTypingAsync();
|
await msg.Channel.TriggerTypingAsync();
|
||||||
await msg.RespondAsync(string.Join('\n',
|
resp = string.Join('\n', CommandHandler.GetResult(msg.Content[mentionPrefix.Length..].TrimStart()).ReturnStrings);
|
||||||
CommandHandler.GetResult(msg.Content[mentionPrefix.Length..]).ReturnStrings));
|
}
|
||||||
|
if (resp != null)
|
||||||
|
{
|
||||||
|
foreach (string mesg in SplitMessage(resp))
|
||||||
|
await msg.RespondAsync(mesg);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.Error(e);
|
Logger.Error(e);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -11,7 +11,7 @@ using Timer = System.Timers.Timer;
|
||||||
Server.Server server = new Server.Server();
|
Server.Server server = new Server.Server();
|
||||||
HashSet<int> shineBag = new HashSet<int>();
|
HashSet<int> shineBag = new HashSet<int>();
|
||||||
CancellationTokenSource cts = new CancellationTokenSource();
|
CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
Task listenTask = server.Listen(cts.Token);
|
bool restartRequested = false;
|
||||||
Logger consoleLogger = new Logger("Console");
|
Logger consoleLogger = new Logger("Console");
|
||||||
DiscordBot bot = new DiscordBot();
|
DiscordBot bot = new DiscordBot();
|
||||||
await bot.Run();
|
await bot.Run();
|
||||||
|
@ -48,6 +48,10 @@ async Task LoadShines()
|
||||||
|
|
||||||
if (loadedShines is not null) shineBag = loadedShines;
|
if (loadedShines is not null) shineBag = loadedShines;
|
||||||
}
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
consoleLogger.Error(ex);
|
consoleLogger.Error(ex);
|
||||||
|
@ -58,34 +62,24 @@ async Task LoadShines()
|
||||||
await LoadShines();
|
await LoadShines();
|
||||||
|
|
||||||
server.ClientJoined += (c, _) => {
|
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["shineSync"] = new ConcurrentBag<int>();
|
||||||
c.Metadata["loadedSave"] = false;
|
c.Metadata["loadedSave"] = false;
|
||||||
c.Metadata["scenario"] = (byte?) 0;
|
c.Metadata["scenario"] = (byte?) 0;
|
||||||
c.Metadata["2d"] = false;
|
c.Metadata["2d"] = false;
|
||||||
c.Metadata["speedrun"] = false;
|
c.Metadata["disableShineSync"] = 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) {
|
async Task ClientSyncShineBag(Client client) {
|
||||||
|
if (!Settings.Instance.Shines.Enabled) return;
|
||||||
try {
|
try {
|
||||||
if ((bool?) client.Metadata["speedrun"] ?? false) return;
|
if ((bool?) client.Metadata["disableShineSync"] ?? false) return;
|
||||||
ConcurrentBag<int> clientBag = (ConcurrentBag<int>) (client.Metadata["shineSync"] ??= new ConcurrentBag<int>());
|
ConcurrentBag<int> clientBag = (ConcurrentBag<int>) (client.Metadata["shineSync"] ??= new ConcurrentBag<int>());
|
||||||
foreach (int shine in shineBag.Except(clientBag).ToArray()) {
|
foreach (int shine in shineBag.Except(clientBag).Except(Settings.Instance.Shines.Excluded).ToArray()) {
|
||||||
clientBag.Add(shine);
|
if (!client.Connected) return;
|
||||||
await client.Send(new ShinePacket {
|
await client.Send(new ShinePacket {
|
||||||
ShineId = shine
|
ShineId = shine
|
||||||
});
|
});
|
||||||
|
clientBag.Add(shine);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// errors that can happen when sending will crash the server :)
|
// errors that can happen when sending will crash the server :)
|
||||||
|
@ -95,7 +89,7 @@ async Task ClientSyncShineBag(Client client) {
|
||||||
async void SyncShineBag() {
|
async void SyncShineBag() {
|
||||||
try {
|
try {
|
||||||
await PersistShines();
|
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 {
|
} catch {
|
||||||
// errors that can happen shines change will crash the server :)
|
// errors that can happen shines change will crash the server :)
|
||||||
}
|
}
|
||||||
|
@ -109,58 +103,127 @@ timer.Start();
|
||||||
|
|
||||||
float MarioSize(bool is2d) => is2d ? 180 : 160;
|
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) => {
|
server.PacketHandler = (c, p) => {
|
||||||
switch (p) {
|
switch (p) {
|
||||||
case GamePacket gamePacket: {
|
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}");
|
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["scenario"] = gamePacket.ScenarioNum;
|
||||||
c.Metadata["2d"] = gamePacket.Is2d;
|
c.Metadata["2d"] = gamePacket.Is2d;
|
||||||
c.Metadata["lastGamePacket"] = gamePacket;
|
c.Metadata["lastGamePacket"] = gamePacket;
|
||||||
|
|
||||||
switch (gamePacket.Stage) {
|
switch (gamePacket.Stage) {
|
||||||
case "CapWorldHomeStage" when gamePacket.ScenarioNum == 0:
|
case "CapWorldHomeStage" when gamePacket.ScenarioNum == 1:
|
||||||
c.Metadata["speedrun"] = true;
|
case "CapWorldTowerStage" when gamePacket.ScenarioNum == 1:
|
||||||
|
if (!((bool?) c.Metadata["disableShineSync"] ?? false)) {
|
||||||
|
c.Metadata["disableShineSync"] = true;
|
||||||
((ConcurrentBag<int>) (c.Metadata["shineSync"] ??= new ConcurrentBag<int>())).Clear();
|
((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");
|
c.Logger.Info("Entered Cap on new save, preventing moon sync until Cascade");
|
||||||
|
if (Settings.Instance.Shines.ClearOnNewSaves) {
|
||||||
|
shineBag.Clear();
|
||||||
|
c.Logger.Info("Cleared shine bags");
|
||||||
|
Task.Run(PersistShines);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "WaterfallWorldHomeStage":
|
default:
|
||||||
bool wasSpeedrun = (bool) c.Metadata["speedrun"]!;
|
if ((bool?) c.Metadata["disableShineSync"] ?? false) {
|
||||||
c.Metadata["speedrun"] = false;
|
|
||||||
if (wasSpeedrun)
|
|
||||||
Task.Run(async () => {
|
Task.Run(async () => {
|
||||||
c.Logger.Info("Entered Cascade with moon sync disabled, enabling moon sync");
|
c.Logger.Info("Entered Cascade or later with moon sync disabled, enabling moon sync again");
|
||||||
await Task.Delay(15000);
|
await Task.Delay(2000);
|
||||||
|
c.Metadata["disableShineSync"] = false;
|
||||||
await ClientSyncShineBag(c);
|
await ClientSyncShineBag(c);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.Instance.Scenario.MergeEnabled) {
|
if (Settings.Instance.Scenario.MergeEnabled) {
|
||||||
server.BroadcastReplace(gamePacket, c, (from, to, gp) => {
|
server.BroadcastReplace(gamePacket, c, (from, to, gp) => {
|
||||||
gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200;
|
gp.ScenarioNum = (byte?) to.Metadata["scenario"] ?? 200;
|
||||||
to.Send(gp, from);
|
#pragma warning disable CS4014
|
||||||
|
to.Send(gp, from).ContinueWith(logError);
|
||||||
|
#pragma warning restore CS4014
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore all other packets from ignored players
|
||||||
|
case IPacket pack when c.Ignored: {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
case TagPacket tagPacket: {
|
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.State) != 0) c.Metadata["seeking"] = tagPacket.IsIt;
|
||||||
if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0)
|
if ((tagPacket.UpdateType & TagPacket.TagUpdate.Time) != 0)
|
||||||
c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now);
|
c.Metadata["time"] = new Time(tagPacket.Minutes, tagPacket.Seconds, DateTime.Now);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CostumePacket:
|
|
||||||
ClientSyncShineBag(c);
|
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;
|
c.Metadata["loadedSave"] = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ShinePacket shinePacket: {
|
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;
|
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);
|
shineBag.Add(shinePacket.ShineId);
|
||||||
if (playerBag.Contains(shinePacket.ShineId)) break;
|
if (playerBag.Contains(shinePacket.ShineId)) break;
|
||||||
c.Logger.Info($"Got moon {shinePacket.ShineId}");
|
c.Logger.Info($"Got moon {shinePacket.ShineId}");
|
||||||
|
@ -168,135 +231,157 @@ server.PacketHandler = (c, p) => {
|
||||||
SyncShineBag();
|
SyncShineBag();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled
|
|
||||||
|
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.Pov is FlipOptions.Both or FlipOptions.Others
|
||||||
&& Settings.Instance.Flip.Players.Contains(c.Id): {
|
&& Settings.Instance.Flip.Players.Contains(c.Id)
|
||||||
playerPacket.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]);
|
) {
|
||||||
playerPacket.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
|
flipPlayer(c, ref playerPacket);
|
||||||
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
#pragma warning disable CS4014
|
||||||
server.Broadcast(playerPacket, c);
|
server.Broadcast(playerPacket, c).ContinueWith(logError);
|
||||||
|
#pragma warning restore CS4014
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case PlayerPacket playerPacket when Settings.Instance.Flip.Enabled
|
// flip only for specific clients
|
||||||
|
if ( Settings.Instance.Flip.Enabled
|
||||||
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self
|
&& Settings.Instance.Flip.Pov is FlipOptions.Both or FlipOptions.Self
|
||||||
&& !Settings.Instance.Flip.Players.Contains(c.Id): {
|
&& !Settings.Instance.Flip.Players.Contains(c.Id)
|
||||||
|
) {
|
||||||
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
|
server.BroadcastReplace(playerPacket, c, (from, to, sp) => {
|
||||||
if (Settings.Instance.Flip.Players.Contains(to.Id)) {
|
if (Settings.Instance.Flip.Players.Contains(to.Id)) {
|
||||||
sp.Position += Vector3.UnitY * MarioSize((bool) c.Metadata["2d"]);
|
flipPlayer(c, ref sp);
|
||||||
sp.Rotation *= Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationX(MathF.PI))
|
|
||||||
* Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateRotationY(MathF.PI));
|
|
||||||
}
|
}
|
||||||
|
#pragma warning disable CS4014
|
||||||
to.Send(sp, from);
|
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] == "*") {
|
||||||
|
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 => {
|
CommandHandler.RegisterCommand("rejoin", args => {
|
||||||
if (args.Length == 0) {
|
if (args.Length == 0) {
|
||||||
return "Usage: rejoin <* | usernames...>";
|
return "Usage: rejoin <* | !* (usernames to not rejoin...) | (usernames to rejoin...)>";
|
||||||
}
|
|
||||||
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 clients.Length > 0 ? $"Caused {builder} to rejoin" : "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) {
|
||||||
|
user.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
});
|
});
|
||||||
|
|
||||||
CommandHandler.RegisterCommand("crash", args => {
|
CommandHandler.RegisterCommand("crash", args => {
|
||||||
if (args.Length == 0) {
|
if (args.Length == 0) {
|
||||||
return "Usage: crash <* | usernames...>";
|
return "Usage: crash <* | !* (usernames to not crash...) | (usernames to crash...)>";
|
||||||
}
|
}
|
||||||
bool moreThanOne = false;
|
|
||||||
StringBuilder builder = new StringBuilder();
|
var res = MultiUserCommandHelper(args);
|
||||||
Client[] clients = (args[0].Trim() == "*"
|
|
||||||
? server.Clients.Where(c => c.Connected)
|
StringBuilder sb = new StringBuilder();
|
||||||
: server.Clients.Where(c =>
|
sb.Append(res.toActUpon.Count > 0 ? "Crashed: " + string.Join(", ", res.toActUpon.Select(x => $"\"{x.Name}\"")) : "");
|
||||||
c.Connected && args.Any(x => c.Name == x || (Guid.TryParse(x, out Guid result) && result == c.Id)))).ToArray();
|
sb.Append(res.failToFind.Count > 0 ? "\nFailed to find matches for: " + string.Join(", ", res.failToFind.Select(x => $"\"{x.ToLower()}\"")) : "");
|
||||||
foreach (Client user in clients) {
|
if (res.ambig.Count > 0) {
|
||||||
if (moreThanOne) builder.Append(", ");
|
res.ambig.ForEach(x => {
|
||||||
moreThanOne = true;
|
sb.Append($"\nAmbiguous for \"{x.arg}\": {string.Join(", ", x.amb.Select(x => $"\"{x}\""))}");
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return clients.Length > 0 ? $"Crashed {builder}" : "Usage: crash <usernames...>";
|
foreach (Client user in res.toActUpon) {
|
||||||
|
BanLists.Crash(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
});
|
});
|
||||||
|
|
||||||
CommandHandler.RegisterCommand("ban", args => {
|
CommandHandler.RegisterCommand("ban", args => { return BanLists.HandleBanCommand(args, (args) => MultiUserCommandHelper(args)); });
|
||||||
if (args.Length == 0) {
|
CommandHandler.RegisterCommand("unban", args => { return BanLists.HandleUnbanCommand(args); });
|
||||||
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 => {
|
CommandHandler.RegisterCommand("send", args => {
|
||||||
const string optionUsage = "Usage: send <stage> <id> <scenario[-1..127]> <player/*>";
|
const string optionUsage = "Usage: send <stage> <id> <scenario[-1..127]> <player/*>";
|
||||||
if (args.Length < 4)
|
if (args.Length < 4) {
|
||||||
return optionUsage;
|
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];
|
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)
|
if (!sbyte.TryParse(args[2], out sbyte scenario) || scenario < -1)
|
||||||
return $"Invalid scenario number {args[2]} (range: [-1 to 127])";
|
return $"Invalid scenario number {args[2]} (range: [-1 to 127])";
|
||||||
Client[] players = args[3] == "*"
|
Client[] players = args[3] == "*"
|
||||||
|
@ -318,17 +403,13 @@ CommandHandler.RegisterCommand("send", args => {
|
||||||
|
|
||||||
CommandHandler.RegisterCommand("sendall", args => {
|
CommandHandler.RegisterCommand("sendall", args => {
|
||||||
const string optionUsage = "Usage: sendall <stage>";
|
const string optionUsage = "Usage: sendall <stage>";
|
||||||
if (args.Length < 1)
|
if (args.Length < 1) {
|
||||||
return optionUsage;
|
return optionUsage;
|
||||||
|
|
||||||
string stage = args[0];
|
|
||||||
|
|
||||||
if (Constants.MapNames.TryGetValue(stage.ToLower(), out string? mapName)) {
|
|
||||||
stage = mapName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stage.Contains("Stage") && !stage.Contains("Zone")) {
|
string? stage = Stages.Input2Stage(args[0]);
|
||||||
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 (stage == null) {
|
||||||
|
return "Invalid Stage Name! ```" + Stages.KingdomAliasMapping() + "```";
|
||||||
}
|
}
|
||||||
|
|
||||||
Client[] players = server.Clients.Where(c => c.Connected).ToArray();
|
Client[] players = server.Clients.Where(c => c.Connected).ToArray();
|
||||||
|
@ -383,12 +464,17 @@ CommandHandler.RegisterCommand("tag", args => {
|
||||||
TagPacket tagPacket = new TagPacket {
|
TagPacket tagPacket = new TagPacket {
|
||||||
UpdateType = TagPacket.TagUpdate.Time,
|
UpdateType = TagPacket.TagUpdate.Time,
|
||||||
Minutes = minutes,
|
Minutes = minutes,
|
||||||
Seconds = seconds
|
Seconds = seconds,
|
||||||
};
|
};
|
||||||
if (args[1] == "*")
|
if (args[1] == "*") {
|
||||||
server.Broadcast(tagPacket);
|
Parallel.ForEachAsync(server.Clients, async (client, _) => {
|
||||||
else
|
await server.Broadcast(tagPacket, client);
|
||||||
client?.Send(tagPacket);
|
await client.Send(tagPacket);
|
||||||
|
});
|
||||||
|
} else if (client != null) {
|
||||||
|
server.Broadcast(tagPacket, client);
|
||||||
|
client.Send(tagPacket);
|
||||||
|
}
|
||||||
return $"Set time for {(args[1] == "*" ? "everyone" : args[1])} to {minutes}:{seconds}";
|
return $"Set time for {(args[1] == "*" ? "everyone" : args[1])} to {minutes}:{seconds}";
|
||||||
}
|
}
|
||||||
case "seeking" when args.Length == 3: {
|
case "seeking" when args.Length == 3: {
|
||||||
|
@ -397,12 +483,17 @@ CommandHandler.RegisterCommand("tag", args => {
|
||||||
if (!bool.TryParse(args[2], out bool seeking)) return $"Usage: tag seeking {args[1]} <true/false>";
|
if (!bool.TryParse(args[2], out bool seeking)) return $"Usage: tag seeking {args[1]} <true/false>";
|
||||||
TagPacket tagPacket = new TagPacket {
|
TagPacket tagPacket = new TagPacket {
|
||||||
UpdateType = TagPacket.TagUpdate.State,
|
UpdateType = TagPacket.TagUpdate.State,
|
||||||
IsIt = seeking
|
IsIt = seeking,
|
||||||
};
|
};
|
||||||
if (args[1] == "*")
|
if (args[1] == "*") {
|
||||||
server.Broadcast(tagPacket);
|
Parallel.ForEachAsync(server.Clients, async (client, _) => {
|
||||||
else
|
await server.Broadcast(tagPacket, client);
|
||||||
client?.Send(tagPacket);
|
await client.Send(tagPacket);
|
||||||
|
});
|
||||||
|
} else if (client != null) {
|
||||||
|
server.Broadcast(tagPacket, client);
|
||||||
|
client.Send(tagPacket);
|
||||||
|
}
|
||||||
return $"Set {(args[1] == "*" ? "everyone" : args[1])} to {(seeking ? "seeker" : "hider")}";
|
return $"Set {(args[1] == "*" ? "everyone" : args[1])} to {(seeking ? "seeker" : "hider")}";
|
||||||
}
|
}
|
||||||
case "start" when args.Length > 2: {
|
case "start" when args.Length > 2: {
|
||||||
|
@ -416,17 +507,22 @@ CommandHandler.RegisterCommand("tag", args => {
|
||||||
int realTime = 1000 * time;
|
int realTime = 1000 * time;
|
||||||
await Task.Delay(realTime);
|
await Task.Delay(realTime);
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
Parallel.ForEachAsync(seekers, async (seeker, _) =>
|
Parallel.ForEachAsync(seekers, async (seeker, _) => {
|
||||||
await server.Broadcast(new TagPacket {
|
TagPacket packet = new TagPacket {
|
||||||
UpdateType = TagPacket.TagUpdate.State,
|
UpdateType = TagPacket.TagUpdate.State,
|
||||||
IsIt = true
|
IsIt = true,
|
||||||
}, seeker)),
|
};
|
||||||
Parallel.ForEachAsync(server.Clients.Except(seekers), async (hider, _) =>
|
await server.Broadcast(packet, seeker);
|
||||||
await server.Broadcast(new TagPacket {
|
await seeker.Send(packet);
|
||||||
|
}),
|
||||||
|
Parallel.ForEachAsync(server.Clients.Except(seekers), async (hider, _) => {
|
||||||
|
TagPacket packet = new TagPacket {
|
||||||
UpdateType = TagPacket.TagUpdate.State,
|
UpdateType = TagPacket.TagUpdate.State,
|
||||||
IsIt = false
|
IsIt = false,
|
||||||
}, hider)
|
};
|
||||||
)
|
await server.Broadcast(packet, hider);
|
||||||
|
await hider.Send(packet);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
consoleLogger.Info($"Started game with seekers {string.Join(", ", seekerNames)}");
|
consoleLogger.Info($"Started game with seekers {string.Join(", ", seekerNames)}");
|
||||||
});
|
});
|
||||||
|
@ -503,16 +599,22 @@ CommandHandler.RegisterCommand("flip", args => {
|
||||||
});
|
});
|
||||||
|
|
||||||
CommandHandler.RegisterCommand("shine", args => {
|
CommandHandler.RegisterCommand("shine", args => {
|
||||||
const string optionUsage = "Valid options: list, clear, sync, send";
|
const string optionUsage = "Valid options: list, clear, sync, send, set, include, exclude";
|
||||||
if (args.Length < 1)
|
if (args.Length < 1)
|
||||||
return optionUsage;
|
return optionUsage;
|
||||||
switch (args[0]) {
|
switch (args[0]) {
|
||||||
case "list" when args.Length == 1:
|
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:
|
case "clear" when args.Length == 1:
|
||||||
shineBag.Clear();
|
shineBag.Clear();
|
||||||
|
Task.Run(PersistShines);
|
||||||
|
|
||||||
foreach (ConcurrentBag<int> playerBag in server.Clients.Select(serverClient =>
|
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";
|
return "Cleared shine bags";
|
||||||
case "sync" when args.Length == 1:
|
case "sync" when args.Length == 1:
|
||||||
|
@ -532,6 +634,30 @@ CommandHandler.RegisterCommand("shine", args => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return optionUsage;
|
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:
|
default:
|
||||||
return optionUsage;
|
return optionUsage;
|
||||||
}
|
}
|
||||||
|
@ -542,6 +668,21 @@ CommandHandler.RegisterCommand("loadsettings", _ => {
|
||||||
return "Loaded settings.json";
|
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) => {
|
Console.CancelKeyPress += (_, e) => {
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
consoleLogger.Info("Received Ctrl+C");
|
consoleLogger.Info("Received Ctrl+C");
|
||||||
|
@ -553,6 +694,7 @@ CommandHandler.RegisterCommandAliases(_ => {
|
||||||
return "Shutting down";
|
return "Shutting down";
|
||||||
}, "exit", "quit", "q");
|
}, "exit", "quit", "q");
|
||||||
|
|
||||||
|
#pragma warning disable CS4014
|
||||||
Task.Run(() => {
|
Task.Run(() => {
|
||||||
consoleLogger.Info("Run help command for valid commands.");
|
consoleLogger.Info("Run help command for valid commands.");
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -563,6 +705,20 @@ Task.Run(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}).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()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
257
Server/Server.cs
257
Server/Server.cs
|
@ -31,8 +31,12 @@ public class Server {
|
||||||
|
|
||||||
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
|
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
|
||||||
|
|
||||||
|
// start sub thread to handle client
|
||||||
try {
|
try {
|
||||||
Task.Run(() => HandleSocket(socket));
|
#pragma warning disable CS4014
|
||||||
|
Task.Run(() => HandleSocket(socket))
|
||||||
|
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
|
||||||
|
#pragma warning restore CS4014
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
Logger.Error($"Error occured while setting up socket handler? {e}");
|
Logger.Error($"Error occured while setting up socket handler? {e}");
|
||||||
|
@ -55,6 +59,7 @@ public class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Info("Server closed");
|
Logger.Info("Server closed");
|
||||||
|
Console.WriteLine("\n\n\n"); //for the sake of the restart command.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +74,9 @@ public class Server {
|
||||||
public delegate void PacketReplacer<in T>(Client from, Client to, T value); // replacer must send
|
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 {
|
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 {
|
public async Task Broadcast<T>(T packet, Client sender) where T : struct, IPacket {
|
||||||
|
@ -77,19 +84,19 @@ public class Server {
|
||||||
PacketHeader header = new PacketHeader {
|
PacketHeader header = new PacketHeader {
|
||||||
Id = sender?.Id ?? Guid.Empty,
|
Id = sender?.Id ?? Guid.Empty,
|
||||||
Type = Constants.PacketMap[typeof(T)].Type,
|
Type = Constants.PacketMap[typeof(T)].Type,
|
||||||
PacketSize = packet.Size
|
PacketSize = packet.Size,
|
||||||
};
|
};
|
||||||
FillPacket(header, packet, memory.Memory);
|
FillPacket(header, packet, memory.Memory);
|
||||||
await Broadcast(memory, sender);
|
await Broadcast(memory, sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Broadcast<T>(T packet) where T : struct, IPacket {
|
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);
|
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
|
||||||
PacketHeader header = new PacketHeader {
|
PacketHeader header = new PacketHeader {
|
||||||
Id = client.Id,
|
Id = client.Id,
|
||||||
Type = Constants.PacketMap[typeof(T)].Type,
|
Type = Constants.PacketMap[typeof(T)].Type,
|
||||||
PacketSize = packet.Size
|
PacketSize = packet.Size,
|
||||||
};
|
};
|
||||||
FillPacket(header, packet, memory.Memory);
|
FillPacket(header, packet, memory.Memory);
|
||||||
await client.Send(memory.Memory, client);
|
await client.Send(memory.Memory, client);
|
||||||
|
@ -103,7 +110,7 @@ public class Server {
|
||||||
/// <param name="data">Memory owner to dispose once done</param>
|
/// <param name="data">Memory owner to dispose once done</param>
|
||||||
/// <param name="sender">Optional sender to not broadcast data to</param>
|
/// <param name="sender">Optional sender to not broadcast data to</param>
|
||||||
public async Task Broadcast(IMemoryOwner<byte> data, Client? sender = null) {
|
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();
|
data.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +120,7 @@ public class Server {
|
||||||
/// <param name="data">Memory to send to the clients</param>
|
/// <param name="data">Memory to send to the clients</param>
|
||||||
/// <param name="sender">Optional sender to not broadcast data to</param>
|
/// <param name="sender">Optional sender to not broadcast data to</param>
|
||||||
public async void Broadcast(Memory<byte> data, Client? sender = null) {
|
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) {
|
public Client? FindExistingClient(Guid id) {
|
||||||
|
@ -123,10 +130,9 @@ public class Server {
|
||||||
|
|
||||||
private async void HandleSocket(Socket socket) {
|
private async void HandleSocket(Socket socket) {
|
||||||
Client client = new Client(socket) {Server = this};
|
Client client = new Client(socket) {Server = this};
|
||||||
|
var remote = socket.RemoteEndPoint;
|
||||||
IMemoryOwner<byte> memory = null!;
|
IMemoryOwner<byte> memory = null!;
|
||||||
await client.Send(new InitPacket {
|
|
||||||
MaxPlayers = Settings.Instance.Server.MaxPlayers
|
|
||||||
});
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -138,7 +144,7 @@ public class Server {
|
||||||
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
|
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
// treat it as a disconnect and exit
|
// 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);
|
if (socket.Connected) await socket.DisconnectAsync(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -149,8 +155,9 @@ public class Server {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0))
|
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
|
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
|
||||||
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
|
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
|
||||||
if (header.PacketSize > 0) {
|
if (header.PacketSize > 0) {
|
||||||
|
@ -158,67 +165,101 @@ public class Server {
|
||||||
memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize);
|
memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize);
|
||||||
memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]);
|
memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]);
|
||||||
memTemp.Dispose();
|
memTemp.Dispose();
|
||||||
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize))
|
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// connection initialization
|
// connection initialization
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false; // only do this once
|
||||||
if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
|
|
||||||
|
// 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();
|
ConnectPacket connect = new ConnectPacket();
|
||||||
connect.Deserialize(memory.Memory.Span[packetRange]);
|
connect.Deserialize(memory.Memory.Span[packetRange]);
|
||||||
lock (Clients) {
|
|
||||||
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();
|
|
||||||
goto disconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool firstConn = false;
|
|
||||||
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.Reconnecting: {
|
|
||||||
client.Id = header.Id;
|
client.Id = header.Id;
|
||||||
if (FindExistingClient(header.Id) is { } newClient) {
|
client.Name = connect.ClientName;
|
||||||
if (newClient.Connected) {
|
|
||||||
newClient.Logger.Info($"Disconnecting already connected client {newClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
|
// is the IPv4 address banned?
|
||||||
newClient.Dispose();
|
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;
|
||||||
}
|
}
|
||||||
newClient.Socket = client.Socket;
|
// is the profile ID banned?
|
||||||
client = newClient;
|
else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) {
|
||||||
} else {
|
client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})");
|
||||||
firstConn = true;
|
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;
|
||||||
|
memory.Dispose();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect and handle reconnections
|
||||||
|
bool isClientNew = true;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
|
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default: {
|
||||||
throw new Exception($"Invalid connection type {connect.ConnectionType}");
|
throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Connected = true;
|
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);
|
Clients.Add(client);
|
||||||
|
|
||||||
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
|
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
|
||||||
|
@ -226,24 +267,33 @@ public class Server {
|
||||||
|
|
||||||
ClientJoined?.Invoke(client, connect);
|
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, _) => {
|
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));
|
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 {
|
PacketHeader connectHeader = new PacketHeader {
|
||||||
Id = other.Id,
|
Id = other.Id,
|
||||||
Type = PacketType.Connect,
|
Type = PacketType.Connect,
|
||||||
PacketSize = connect.Size
|
PacketSize = connect.Size,
|
||||||
};
|
};
|
||||||
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
|
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
|
||||||
ConnectPacket connectPacket = new ConnectPacket {
|
ConnectPacket connectPacket = new ConnectPacket {
|
||||||
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
|
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
|
||||||
MaxPlayers = Settings.Instance.Server.MaxPlayers,
|
MaxPlayers = Settings.Instance.Server.MaxPlayers,
|
||||||
ClientName = other.Name
|
ClientName = other.Name,
|
||||||
};
|
};
|
||||||
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
|
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
|
||||||
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
|
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
|
||||||
|
|
||||||
|
// tell the new client what costume the other client has
|
||||||
if (other.CurrentCostume.HasValue) {
|
if (other.CurrentCostume.HasValue) {
|
||||||
connectHeader.Type = PacketType.Costume;
|
connectHeader.Type = PacketType.Costume;
|
||||||
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
|
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
|
||||||
|
@ -253,25 +303,30 @@ public class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
tempBuffer.Dispose();
|
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.");
|
Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected.");
|
||||||
} else if (header.Id != client.Id && client.Id != Guid.Empty) {
|
|
||||||
|
// 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}");
|
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 {
|
try {
|
||||||
|
// parse the packet
|
||||||
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
|
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
|
||||||
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
|
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
|
||||||
|
|
||||||
|
// process the packet
|
||||||
if (PacketHandler?.Invoke(client, packet) is false) {
|
if (PacketHandler?.Invoke(client, packet) is false) {
|
||||||
|
// don't broadcast the packet to everyone
|
||||||
memory.Dispose();
|
memory.Dispose();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -280,31 +335,87 @@ public class Server {
|
||||||
client.Logger.Error($"Packet handler warning: {e}");
|
client.Logger.Error($"Packet handler warning: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Broadcast(memory, client);
|
#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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
|
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
|
||||||
client.Logger.Info($"Disconnected from the server: Connection reset");
|
client.Logger.Info($"Disconnected from the server: Connection reset");
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
client.Logger.Error($"Disconnecting due to exception: {e}");
|
client.Logger.Error($"Disconnecting due to exception: {e}");
|
||||||
if (socket.Connected) Task.Run(() => socket.DisconnectAsync(false));
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memory?.Dispose();
|
memory?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect:
|
// client disconnected
|
||||||
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server");
|
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;
|
client.Connected = false;
|
||||||
try {
|
try {
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
catch { /*lol*/ }
|
catch { /*lol*/ }
|
||||||
|
|
||||||
Task.Run(() => Broadcast(new DisconnectPacket(), client));
|
#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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PacketHeader GetHeader(Span<byte> data) {
|
private static PacketHeader GetHeader(Span<byte> data) {
|
||||||
|
|
|
@ -30,10 +30,10 @@ public class Settings {
|
||||||
LoadHandler?.Invoke();
|
LoadHandler?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SaveSettings() {
|
public static void SaveSettings(bool silent = false) {
|
||||||
try {
|
try {
|
||||||
File.WriteAllText("settings.json", JsonConvert.SerializeObject(Instance, Formatting.Indented, new StringEnumConverter(new CamelCaseNamingStrategy())));
|
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) {
|
catch (Exception e) {
|
||||||
Logger.Error($"Failed to save settings.json {e}");
|
Logger.Error($"Failed to save settings.json {e}");
|
||||||
|
@ -43,8 +43,9 @@ public class Settings {
|
||||||
public ServerTable Server { get; set; } = new ServerTable();
|
public ServerTable Server { get; set; } = new ServerTable();
|
||||||
public FlipTable Flip { get; set; } = new FlipTable();
|
public FlipTable Flip { get; set; } = new FlipTable();
|
||||||
public ScenarioTable Scenario { get; set; } = new ScenarioTable();
|
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 DiscordTable Discord { get; set; } = new DiscordTable();
|
||||||
|
public ShineTable Shines { get; set; } = new ShineTable();
|
||||||
public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable();
|
public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable();
|
||||||
|
|
||||||
public class ServerTable {
|
public class ServerTable {
|
||||||
|
@ -57,24 +58,32 @@ public class Settings {
|
||||||
public bool MergeEnabled { get; set; } = false;
|
public bool MergeEnabled { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BannedPlayers {
|
public class BanListTable {
|
||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
public List<Guid> Players { get; set; } = new List<Guid>();
|
public ISet<Guid> Players { get; set; } = new SortedSet<Guid>();
|
||||||
public List<string> IpAddresses { get; set; } = new List<string>();
|
public ISet<string> IpAddresses { get; set; } = new SortedSet<string>();
|
||||||
|
public ISet<string> Stages { get; set; } = new SortedSet<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FlipTable {
|
public class FlipTable {
|
||||||
public bool Enabled { get; set; } = true;
|
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;
|
public FlipOptions Pov { get; set; } = FlipOptions.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DiscordTable {
|
public class DiscordTable {
|
||||||
public string? Token { get; set; }
|
public string? Token { get; set; }
|
||||||
public string Prefix { get; set; } = "$";
|
public string Prefix { get; set; } = "$";
|
||||||
|
public string? CommandChannel { get; set; }
|
||||||
public string? LogChannel { 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 bool ClearOnNewSaves { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
public class PersistShinesTable
|
public class PersistShinesTable
|
||||||
{
|
{
|
||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
|
|
|
@ -21,24 +21,4 @@ public static class Constants {
|
||||||
.ToDictionary(type => type.GetCustomAttribute<PacketAttribute>()!.Type, type => type);
|
.ToDictionary(type => type.GetCustomAttribute<PacketAttribute>()!.Type, type => type);
|
||||||
|
|
||||||
public static int HeaderSize { get; } = PacketHeader.StaticSize;
|
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"}
|
|
||||||
};
|
|
||||||
}
|
}
|
|
@ -9,20 +9,20 @@ public struct TagPacket : IPacket {
|
||||||
public byte Seconds;
|
public byte Seconds;
|
||||||
public ushort Minutes;
|
public ushort Minutes;
|
||||||
|
|
||||||
public short Size => 6;
|
public short Size => 5;
|
||||||
|
|
||||||
public void Serialize(Span<byte> data) {
|
public void Serialize(Span<byte> data) {
|
||||||
MemoryMarshal.Write(data, ref UpdateType);
|
MemoryMarshal.Write(data, ref UpdateType);
|
||||||
MemoryMarshal.Write(data[1..], ref IsIt);
|
MemoryMarshal.Write(data[1..], ref IsIt);
|
||||||
MemoryMarshal.Write(data[2..], ref Seconds);
|
MemoryMarshal.Write(data[2..], ref Seconds);
|
||||||
MemoryMarshal.Write(data[4..], ref Minutes);
|
MemoryMarshal.Write(data[3..], ref Minutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Deserialize(ReadOnlySpan<byte> data) {
|
public void Deserialize(ReadOnlySpan<byte> data) {
|
||||||
UpdateType = MemoryMarshal.Read<TagUpdate>(data);
|
UpdateType = MemoryMarshal.Read<TagUpdate>(data);
|
||||||
IsIt = MemoryMarshal.Read<bool>(data[1..]);
|
IsIt = MemoryMarshal.Read<bool>(data[1..]);
|
||||||
Seconds = MemoryMarshal.Read<byte>(data[2..]);
|
Seconds = MemoryMarshal.Read<byte>(data[2..]);
|
||||||
Minutes = MemoryMarshal.Read<ushort>(data[4..]);
|
Minutes = MemoryMarshal.Read<ushort>(data[3..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
|
|
|
@ -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" },
|
||||||
|
};
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ PacketType[] reboundPackets = {
|
||||||
// PacketType.Shine
|
// PacketType.Shine
|
||||||
};
|
};
|
||||||
|
|
||||||
string lastCapture = "";
|
//string lastCapture = ""; //not referenced
|
||||||
List<TcpClient> clients = new List<TcpClient>();
|
List<TcpClient> clients = new List<TcpClient>();
|
||||||
|
|
||||||
async Task S(string n, Guid otherId, Guid ownId) {
|
async Task S(string n, Guid otherId, Guid ownId) {
|
||||||
|
@ -77,13 +77,15 @@ async Task S(string n, Guid otherId, Guid ownId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (type == PacketType.Player) {
|
if (type == PacketType.Player) {
|
||||||
|
#pragma warning disable CS4014
|
||||||
Task.Run(async () => {
|
Task.Run(async () => {
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
header.Id = ownId;
|
header.Id = ownId;
|
||||||
MemoryMarshal.Write(owner.Memory.Span[..Constants.HeaderSize], ref header);
|
MemoryMarshal.Write(owner.Memory.Span[..Constants.HeaderSize], ref header);
|
||||||
await stream.WriteAsync(owner.Memory[..(Constants.HeaderSize + header.PacketSize)]);
|
await stream.WriteAsync(owner.Memory[..(Constants.HeaderSize + header.PacketSize)]);
|
||||||
owner.Dispose();
|
owner.Dispose();
|
||||||
});
|
}).ContinueWith(x => { if (x.Exception != null) { logger.Error(x.Exception.ToString()); } });
|
||||||
|
#pragma warning restore CS4014
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
header.Id = ownId;
|
header.Id = ownId;
|
||||||
|
|
|
@ -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: .
|
#build: .
|
||||||
#user: 1000:1000
|
#user: 1000:1000
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 1027:1027
|
- 1027:1027
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -14,6 +14,11 @@ WorkingDirectory=/home/user/SMOServer/
|
||||||
Restart=always
|
Restart=always
|
||||||
# amount of time to wait before restarting the service
|
# amount of time to wait before restarting the service
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
#TTY Input
|
||||||
|
StandardInput=tty-force
|
||||||
|
TTYVHangup=yes
|
||||||
|
TTYPath=/dev/tty20
|
||||||
|
TTYReset=yes
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
Loading…
Reference in New Issue