Compare commits

...

17 Commits
JL ... main

Author SHA1 Message Date
13e9723626 implement custom `SUE` command for handling api 2025-03-17 14:01:03 -07:00
b378ff2689 Merge pull request 'Buddy Precense Reporting' (#3) from buddy-precense into main
Reviewed-on: https://gitea.milenia.local.alanmoon.net/Moonbase/sodoff-mmo/pulls/3
2025-03-07 18:16:13 -08:00
12981bfa61 Merge pull request 'Messaging Extension Endpoints' (#2) from messaging into main
Reviewed-on: https://gitea.milenia.local.alanmoon.net/Moonbase/sodoff-mmo/pulls/2
2025-03-07 18:16:02 -08:00
a7aae8763e Merge pull request 'Moderation Commands' (#1) from moderation-commands into main
Reviewed-on: https://gitea.milenia.local.alanmoon.net/Moonbase/sodoff-mmo/pulls/1
2025-03-07 18:15:42 -08:00
6060d0ddbf implement buddy precense reporting 2025-03-07 17:47:17 -08:00
bea782504e add support for messaging 2025-03-03 17:01:42 -08:00
aa85aeab32 add exception handling to `ApiWebService`
improve ``BanCommand`` API response handling
2025-03-02 15:54:49 -08:00
1edbbde74c rework `BanCommand` to take in a user id of someone in the room instead of a full guid
edit comment at ``ApiUrl`` in appsettings.json
2025-03-02 15:17:55 -08:00
53db86f3c7 implement `ApiWebService`
change ``ChatMessageHandler`` and ``BanCommand`` to use it
2025-03-02 14:52:58 -08:00
e1c19b792c require authentication
send ``SMF`` if chat ban is detected
2025-02-27 18:57:30 -08:00
998efe5624 add `UserBanType` schema from API
modify ``ChatMessageHandler.Chat`` to check for bans before sending out message
2025-02-27 18:00:46 -08:00
86b9d39266 implement `BanCommand` 2025-02-27 17:39:39 -08:00
Robert Paciorek
783d02d4b2 exploits protection bugfixes 2025-01-04 22:33:25 +00:00
Hipposgrumm
158ac4ee21
Support for Math Blaster Ambassador Funzones (#5)
Added ambassador support (for Math Blaster).
Tons of config options too.

* Moved ambassador room controlling to seperate class.
* More changes to appease the Pull Request.
2024-12-30 10:54:53 +01:00
Hipposgrumm
c06cfb4d17
Support for Eat My Dust (#4)
* Support for Eat My Dust
Zombie Alert manager is stubbed.
Added helpers to allow for weapon MMO.
PlayerData for Eat My Dust

* Zombie Alert Added
* Fix Zombie Alert Timing
* Honestly should probably catch this exception [null room in PM handler]. I foresee it occurring very commonly. While it shouldn't halt the server (except in visual studio debug), it's annoying to see.
2024-12-29 17:05:32 +01:00
Robert Paciorek
d2a11b04e2 bugfix for event version of battle ships 2024-10-12 11:11:11 +00:00
rpaciorek
7d96a03042
implement JL (Join LIMBO) command
Join Limbo command based on Alan Moon PR

---------

Co-authored-by: Alan Moon <alanmoonbase2004@gmail.com>
2024-08-19 21:19:48 +02:00
22 changed files with 636 additions and 219 deletions

View File

@ -1,4 +1,6 @@
using sodoffmmo.Attributes; using System.Net;
using System.Net.Http.Json;
using sodoffmmo.Attributes;
using sodoffmmo.Core; using sodoffmmo.Core;
using sodoffmmo.Data; using sodoffmmo.Data;
using sodoffmmo.Management; using sodoffmmo.Management;
@ -37,6 +39,13 @@ class ChatMessageHandler : CommandHandler {
return; return;
} }
// send an http request to api to check for 'IndefiniteOpenChatBan' or 'TemporaryOpenChatBan'
ApiWebService apiWebService = new();
var banType = apiWebService.CheckForUserBan(client);
if (banType != null && (banType == UserBanType.IndefiniteOpenChatBan || banType == UserBanType.TemporaryOpenChatBan))
{ client.Send(Utils.ArrNetworkPacket(new string[] { "SMF", "-1", "CB", "1", "Sorry, You've Been Banned From Using Type Chat", "1" }, "SMF")); return; }
client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DiplayName), client); client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DiplayName), client);
NetworkObject cmd = new(); NetworkObject cmd = new();

View File

@ -0,0 +1,27 @@
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers;
[ExtensionCommandHandler("SCE")]
class CounterEventHandler : CommandHandler {
public override Task Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"SCE","p":{"NAME":"COUNT"},"r":-1}}
if (client.Room is SpecialRoom room) {
string name = receivedObject.Get<NetworkObject>("p").Get<string>("NAME");
if (name == "COUNT" || name == "COUNT2" || name == "COUNT3") {
int index = name switch {
"COUNT" => 0,
"COUNT2" => 1,
"COUNT3" => 2
};
room.ambassadorGauges[index] = Math.Min(100, room.ambassadorGauges[index]+(1/Configuration.ServerConfiguration.AmbassadorGaugePlayers));
room.Send(Utils.VlNetworkPacket(room.GetRoomVars(), client.Room.Id));
} else {
Console.WriteLine($"Invalid attempt to increment room var {name} in {room.Name}.");
}
}
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,29 @@
using System.Globalization;
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers;
// TODO: This is currently stubbed. Supposed to do something.
// Should probably be done by someone who actually played the game.
[ExtensionCommandHandler("SU")]
public class EMDZombiesUpdateHandler : CommandHandler {
public override Task Handle(Client client, NetworkObject receivedObject) {
return Task.CompletedTask;
}
}
[ExtensionCommandHandler("EN")]
public class EMDZombiesEnterHandler : CommandHandler {
public override Task Handle(Client client, NetworkObject receivedObject) {
return Task.CompletedTask;
}
}
[ExtensionCommandHandler("EX")]
public class EMDZombiesExitHandler : CommandHandler {
public override Task Handle(Client client, NetworkObject receivedObject) {
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,17 @@
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers
{
[ExtensionCommandHandler("JL")]
public class JoinLimboHandler : CommandHandler
{
public override Task Handle(Client client, NetworkObject receivedObject)
{
client.SetRoom(Room.GetOrAdd("LIMBO"));
return Task.CompletedTask;
}
}
}

View File

@ -49,18 +49,6 @@ class LoginHandler : CommandHandler
r2.Add(WorldEvent.Get().EventInfoArray(true)); r2.Add(WorldEvent.Get().EventInfoArray(true));
rl.Add(r2); rl.Add(r2);
NetworkArray r3 = new();
r3.Add(2);
r3.Add("LIMBO");
r3.Add("default");
r3.Add(false);
r3.Add(false);
r3.Add(false);
r3.Add((short)31);
r3.Add((short)10000);
r3.Add(new NetworkArray());
rl.Add(r3);
NetworkObject content = new(); NetworkObject content = new();
content.Add("rl", rl); content.Add("rl", rl);
content.Add("zn", client.PlayerData.ZoneName); content.Add("zn", client.PlayerData.ZoneName);
@ -102,6 +90,7 @@ class LoginHandler : CommandHandler
} }
} catch (Exception ex) { } catch (Exception ex) {
Console.WriteLine($"Authentication exception IID: {client.ClientID} - {ex}"); Console.WriteLine($"Authentication exception IID: {client.ClientID} - {ex}");
Console.WriteLine("This Can Be Ignored If API Is The One Logging In.");
} }
return Configuration.ServerConfiguration.Authentication != AuthenticationMode.Required; // return true on auth err if not Required mode return Configuration.ServerConfiguration.Authentication != AuthenticationMode.Required; // return true on auth err if not Required mode
} }

View File

@ -14,9 +14,13 @@ class RacingPMHandler : CommandHandler
NetworkObject p = new(); NetworkObject p = new();
NetworkArray arr = new(); NetworkArray arr = new();
NetworkObject data = new(); NetworkObject data = new();
data.Add("M", new string[] { string M = receivedObject.Get<NetworkObject>("p").Get<string>("M");
receivedObject.Get<NetworkObject>("p").Get<string>("M") if (M.StartsWith("WF:") || M.StartsWith("WFWD:")) {
}); // When firing weapon in EMD, recieving clients expect userid, but the sending client sends its token instead.
string token = M.Split(':')[1];
M = M.Replace(token, client.PlayerData.Uid);
}
data.Add("M", new string[] {M});
data.Add("MID", client.ClientID); data.Add("MID", client.ClientID);
arr.Add(data); arr.Add(data);
p.Add("arr", arr); p.Add("arr", arr);
@ -24,6 +28,7 @@ class RacingPMHandler : CommandHandler
cmd.Add("p", p); cmd.Add("p", p);
NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize();
if (client.Room != null) // Throws an exception in Eat my Dust when the player fires their weapon before fully in the room.
client.Room.Send(packet); client.Room.Send(packet);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -0,0 +1,37 @@
using System;
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers;
[ExtensionCommandHandler("SMB")]
class SendMessageBoardHandler : CommandHandler
{
public override Task Handle(Client client, NetworkObject receivedObject)
{
NetworkObject args = receivedObject.Get<NetworkObject>("p");
string toUserId = args.Get<string>("tgt");
string content = args.Get<string>("cnt");
string level = args.Get<string>("lvl");
if (toUserId == string.Empty) toUserId = client.PlayerData.Uid; // send to self
ApiWebService apiWebService = new();
// first check for any kind of ban
var banType = apiWebService.CheckForUserBan(client);
if (banType != null && banType >= UserBanType.TemporaryOpenChatBan) { client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); return Task.CompletedTask; }
// send message
var result = apiWebService.SendMessageBoard(client, toUserId, content, level, "0");
if (result)
client.Send(Utils.ArrNetworkPacket(new string[] { "SMA", "-1", "SUCCESS", "1", DateTime.UtcNow.ToString() }, "SMA"));
else client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF"));
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,38 @@
using System;
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers;
[ExtensionCommandHandler("SMR")]
class SendMessageReplyHandler : CommandHandler
{
public override Task Handle(Client client, NetworkObject receivedObject)
{
NetworkObject args = receivedObject.Get<NetworkObject>("p");
string toUserId = args.Get<string>("tgt");
string content = args.Get<string>("cnt");
string level = args.Get<string>("lvl");
string msgId = args.Get<string>("rtm");
if (toUserId == string.Empty) toUserId = client.PlayerData.Uid; // send to self
ApiWebService apiWebService = new();
// first check for any kind of ban
var banType = apiWebService.CheckForUserBan(client);
if (banType != null && banType >= UserBanType.TemporaryOpenChatBan) { client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF")); return Task.CompletedTask; }
// send message
var result = apiWebService.SendMessageBoard(client, toUserId, content, level, msgId);
if (result)
client.Send(Utils.ArrNetworkPacket(new string[] { "SMA", "-1", "SUCCESS", "1", DateTime.UtcNow.ToString() }, "SMA"));
else client.Send(Utils.ArrNetworkPacket(new string[] { "SMF" }, "SMF"));
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using sodoffmmo.Attributes;
using sodoffmmo.Core;
using sodoffmmo.Data;
namespace sodoffmmo.CommandHandlers
{
[ExtensionCommandHandler("SUE")]
public class SendUserEventHandler : CommandHandler
{
public override Task Handle(Client client, NetworkObject receivedObject)
{
NetworkObject p = receivedObject.Get<NetworkObject>("p");
string userId = p.Get<string>("UID");
string cmd = p.Get<string>("CMD");
string[] arr = p.Get<string[]>("ARR");
// find client in all clients list
Client? client1 = Server.AllClients.FirstOrDefault(e => e.PlayerData.Uid == userId);
// send command
if (client1 != null) client1.Send(Utils.ArrNetworkPacket(arr, cmd));
return Task.CompletedTask;
}
}
}

View File

@ -50,8 +50,13 @@ class SetPositionVariablesHandler : CommandHandler {
// user event // user event
string? ue = spvData.Get<string>("UE"); string? ue = spvData.Get<string>("UE");
if (ue != null) if (ue != null) {
long time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if ((time - client.PlayerData.last_ue_time) > 499 || Configuration.ServerConfiguration.AllowChaos) {
vars.Add("UE", ue); vars.Add("UE", ue);
client.PlayerData.last_ue_time = time;
}
}
// pitch // pitch
float? cup = spvData.Get<float?>("CUP"); float? cup = spvData.Get<float?>("CUP");
if (cup != null) if (cup != null)

View File

@ -27,7 +27,7 @@ class SetUserVariablesHandler : CommandHandler {
client.PlayerData.InitFromNetworkData(suvData); client.PlayerData.InitFromNetworkData(suvData);
UpdatePlayersInRoom(); UpdatePlayersInRoom();
SendSUVToPlayerInRoom(); SendSUVToPlayerInRoom();
client.Room.SendAllAlerts(client); if (client.Room is SpecialRoom room) room.SendAllAlerts(client);
} else { } else {
UpdateVars(); UpdateVars();
} }
@ -43,7 +43,7 @@ class SetUserVariablesHandler : CommandHandler {
foreach (string varName in PlayerData.SupportedVariables) { foreach (string varName in PlayerData.SupportedVariables) {
string? value = suvData.Get<string>(varName); string? value = suvData.Get<string>(varName);
if (value != null) { if (value != null) {
client.PlayerData.SetVariable(varName, value); value = client.PlayerData.SetVariable(varName, value);
updated = true; updated = true;
data.Add(varName, value); data.Add(varName, value);
vl.Add(NetworkArray.Param(varName, value)); vl.Add(NetworkArray.Param(varName, value));

View File

@ -10,7 +10,7 @@ class WorldEventStatusHandler : CommandHandler {
public override Task Handle(Client client, NetworkObject receivedObject) { public override Task Handle(Client client, NetworkObject receivedObject) {
client.Send(Utils.ArrNetworkPacket( new string[] { client.Send(Utils.ArrNetworkPacket( new string[] {
"WESR", "WESR",
"WE_ScoutAttack|" + WorldEvent.Get().EventInfo(), "WE_" + Configuration.ServerConfiguration.EventName + "|" + WorldEvent.Get().EventInfo(),
"EvEnd|" + WorldEvent.Get().GetLastResults() "EvEnd|" + WorldEvent.Get().GetLastResults()
})); }));
return Task.CompletedTask; return Task.CompletedTask;

102
src/Core/ApiWebService.cs Normal file
View File

@ -0,0 +1,102 @@
using System;
using System.Net.Http.Json;
using sodoffmmo.CommandHandlers;
namespace sodoffmmo.Core;
public class ApiWebService
{
public UserBanType? CheckForUserBan(Client client)
{
HttpClient httpClient = new();
var content = new FormUrlEncodedContent(
new Dictionary<string, string> {
{ "token", client.PlayerData.UNToken }
}
);
httpClient.Timeout = new TimeSpan(0, 0, 3);
try
{
var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Moderation/CheckForVikingBan", content).Result;
Log("Moderation/CheckForVikingBan");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync<UserBanType>().Result;
else return null;
} catch (Exception e) { LogError(e.Message); return null; }
}
public string? BanUser(Client client, string userId, string banType, string days)
{
HttpClient httpClient = new();
var content = new FormUrlEncodedContent(
new Dictionary<string, string> {
{ "token", client.PlayerData.UNToken },
{ "userId", userId },
{ "banType", banType },
{ "days", days }
}
);
httpClient.Timeout = new TimeSpan(0, 0, 3);
try
{
var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Moderation/AddBanToVikingByGuid", content).Result;
Log("Moderation/AddBanToVikingByGuid");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadAsStringAsync().Result;
else return null;
} catch (Exception e) { LogError(e.Message); return null; }
}
public bool SendMessageBoard(Client client, string userId, string data, string level, string replyMessageId)
{
HttpClient httpClient = new();
var content = new FormUrlEncodedContent
(
new Dictionary<string, string>
{
{ "token", client.PlayerData.UNToken },
{ "userId", userId },
{ "data", data },
{ "messageLevel", level },
{ "replyMessageId", replyMessageId }
}
);
httpClient.Timeout = new TimeSpan(0, 0, 3);
try
{
var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Messaging/PostTextMessage", content).Result;
Log("Messaging/PostTextMessage");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync<bool>().Result;
else return false;
} catch (Exception e) { LogError(e.Message); return false; }
}
public bool SetOnline(Client client, bool online)
{
HttpClient httpClient = new();
var content = new FormUrlEncodedContent
(
new Dictionary<string, string>
{
{ "token", client.PlayerData.UNToken },
{ "online", online.ToString() }
}
);
try
{
var response = httpClient.PostAsync($"{Configuration.ServerConfiguration.ApiUrl}/Precense/SetVikingOnline", content).Result;
Log("Precense/SetVikingOnline");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content != null) return response.Content.ReadFromJsonAsync<bool>().Result;
else return false;
} catch (Exception e) { LogError(e.Message); return false; }
}
private void Log(string endpoint) => Console.WriteLine($"Sent API Request To {Configuration.ServerConfiguration.ApiUrl}/{endpoint}");
private void LogError(string message) => Console.WriteLine($"An Error Has Occured When Sending An API Request - {message}");
}

View File

@ -47,6 +47,9 @@ public class Client {
} }
public void SetRoom(Room? room) { public void SetRoom(Room? room) {
// api web service for setting precense
ApiWebService apiWebService = new();
lock(clientLock) { lock(clientLock) {
// set variable player data as not valid, but do not reset all player data // set variable player data as not valid, but do not reset all player data
PlayerData.IsValid = false; PlayerData.IsValid = false;
@ -59,6 +62,8 @@ public class Client {
data.Add("r", Room.Id); data.Add("r", Room.Id);
data.Add("u", ClientID); data.Add("u", ClientID);
Room.Send(NetworkObject.WrapObject(0, 1004, data).Serialize()); Room.Send(NetworkObject.WrapObject(0, 1004, data).Serialize());
apiWebService.SetOnline(this, false);
} }
// set new room (null when SetRoom is used as LeaveRoom) // set new room (null when SetRoom is used as LeaveRoom)
@ -69,7 +74,9 @@ public class Client {
Room.AddClient(this); Room.AddClient(this);
Send(Room.SubscribeRoom()); Send(Room.SubscribeRoom());
UpdatePlayerUserVariables(); if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo
apiWebService.SetOnline(this, true);
} }
} }
} }
@ -98,11 +105,13 @@ public class Client {
} }
public void ScheduleDisconnect() { public void ScheduleDisconnect() {
ApiWebService apiWebService = new();
if (Room != null) { if (Room != null) {
// quiet remove from room (to avoid issues in Room.Send) // quiet remove from room (to avoid issues in Room.Send)
// - do not change Room value here // - do not change Room value here
// - full remove will be will take place Server.HandleClient (before real disconnected) // - full remove will be will take place Server.HandleClient (before real disconnected)
Room.RemoveClient(this); Room.RemoveClient(this);
apiWebService.SetOnline(this, false);
} }
scheduledDisconnect = true; scheduledDisconnect = true;
} }

View File

@ -33,6 +33,11 @@ internal sealed class ServerConfiguration {
public int EventTimer { get; set; } = 30; public int EventTimer { get; set; } = 30;
public int FirstEventTimer { get; set; } = 10; public int FirstEventTimer { get; set; } = 10;
public Dictionary<string, string[][]> RoomAlerts { get; set; } = new(); public Dictionary<string, string[][]> RoomAlerts { get; set; } = new();
public string[] AmbassadorRooms { get; set; } = Array.Empty<string>();
public int AmbassadorGaugeStart { get; set; } = 75;
public float AmbassadorGaugeDecayRate { get; set; } = 60;
public bool AmbassadorGaugeDecayOnlyWhenInRoom { get; set; } = true;
public float AmbassadorGaugePlayers { get; set; } = 0.5f;
public int RacingMaxPlayers { get; set; } = 6; public int RacingMaxPlayers { get; set; } = 6;
public int RacingMinPlayers { get; set; } = 2; public int RacingMinPlayers { get; set; } = 2;
public int RacingMainLobbyTimer { get; set; } = 15; public int RacingMainLobbyTimer { get; set; } = 15;

View File

@ -111,7 +111,7 @@ public class Room {
roomInfo.Add(false); // is password protected roomInfo.Add(false); // is password protected
roomInfo.Add((short)clients.Count); // player count roomInfo.Add((short)clients.Count); // player count
roomInfo.Add((short)4096); // max player count roomInfo.Add((short)4096); // max player count
roomInfo.Add(RoomVariables); // variables roomInfo.Add(GetRoomVars()); // variables (plus added data)
roomInfo.Add((short)0); // spectator count roomInfo.Add((short)0); // spectator count
roomInfo.Add((short)0); // max spectator count roomInfo.Add((short)0); // max spectator count
@ -143,7 +143,7 @@ public class Room {
r1.Add(false); r1.Add(false);
r1.Add((short)clients.Count); // player count r1.Add((short)clients.Count); // player count
r1.Add((short)4096); // max player count r1.Add((short)4096); // max player count
r1.Add(new NetworkArray()); r1.Add(GetRoomVars());
r1.Add((short)0); r1.Add((short)0);
r1.Add((short)0); r1.Add((short)0);
@ -154,141 +154,5 @@ public class Room {
return NetworkObject.WrapObject(0, 15, obj).Serialize(); return NetworkObject.WrapObject(0, 15, obj).Serialize();
} }
internal virtual NetworkArray GetRoomVars() { return RoomVariables; }
private int alertId = -1;
private Random random = new Random();
List<AlertInfo> alerts = new();
public void AddAlert(AlertInfo alert) {
alerts.Add(alert);
ResetAlertTimer(alert);
}
public void SendAllAlerts(Client client) {
foreach (AlertInfo alert in alerts) {
if (alert.IsRunning()) StartAlert(alert, client);
}
}
private void StartAlert(AlertInfo alert, Client? specificClient = null) {
if (specificClient != null) return; // Disables joining ongoing alerts.
NetworkArray NewRoomVariables = new();
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_START, alertId++, isPersistent: true));
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TYPE, alert.type, isPersistent: true));
double duration = (alert.endTime - DateTime.Now).TotalSeconds;
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_LENGTH, alert.type == "1" ? alert.redAlertDuration : duration, isPersistent: true));
if (alert.type == "1") {
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TIMEOUT, duration, isPersistent: true));
} else if (alert.type == "3") {
alert.songId = random.Next(0, alert.songs);
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_SONG, (double)alert.songId, isPersistent: true));
}
NetworkPacket packet = Utils.VlNetworkPacket(NewRoomVariables, Id);
if (specificClient is null) {
RoomVariables = NewRoomVariables;
Send(packet);
Console.WriteLine("Started event " +alert + " in room " + Name);
} else {
specificClient.Send(packet);
Console.WriteLine("Added " + specificClient.PlayerData.DiplayName + " to event " + alert + " with " + duration + " seconds remaining");
}
}
void ResetAlertTimer(AlertInfo alert) {
System.Timers.Timer? timer = alert.timer;
if (timer != null) {
timer.Stop();
timer.Close();
}
DateTime startTime = DateTime.Now.AddMilliseconds(random.Next(alert.minTime * 1000, alert.maxTime * 1000));
DateTime endTime = startTime.AddSeconds(alert.duration);
for (int i = 0; i < alerts.IndexOf(alert); i++) {
// Prevent overlap between two events.
if (alerts[i].Overlaps(endTime)) {
startTime = alerts[i].endTime.AddSeconds(5);
endTime = startTime.AddSeconds(alert.duration);
}
}
timer = new System.Timers.Timer((startTime - DateTime.Now).TotalMilliseconds);
timer.AutoReset = false;
timer.Enabled = true;
timer.Elapsed += (sender, e) => StartAlert(alert);
timer.Elapsed += (sender, e) => ResetAlertTimer(alert);
alert.timer = timer;
Console.WriteLine("Event " + alert + " in " + Name + " scheduled for " + startTime.ToString("MM/dd/yyyy HH:mm:ss tt") + " (in " + (startTime - DateTime.Now).TotalSeconds + " seconds)");
alert.startTime = startTime;
alert.endTime = endTime;
}
private const string REDALERT_START = "RA_S";
private const string REDALERT_TYPE = "RA_A";
private const string REDALERT_LENGTH = "RA_L";
private const string REDALERT_TIMEOUT = "RA_T";
private const string REDALERT_SONG = "RA_SO";
public class AlertInfo {
public readonly string type;
public readonly double duration;
public readonly int minTime;
public readonly int maxTime;
public readonly int redAlertDuration;
public readonly int songs;
public int songId;
public DateTime startTime {
get {
return newStartTime;
}
set {
oldStartTime = newStartTime;
newStartTime = value;
}
}
public DateTime endTime {
get {
return newEndTime;
}
set {
oldEndTime = newEndTime;
newEndTime = value;
}
}
private DateTime newStartTime;
private DateTime newEndTime;
private DateTime oldStartTime;
private DateTime oldEndTime;
public System.Timers.Timer? timer = null;
public AlertInfo(string type, double duration = 20.0, int minTime = 30, int maxTime = 240, int redAlertDuration = 60, int songs = 16) {
this.type = type;
this.duration = duration;
this.minTime = minTime;
this.maxTime = maxTime;
this.redAlertDuration = redAlertDuration;
this.songs = songs;
}
public bool Overlaps(DateTime time) {
return (time >= oldStartTime && time <= oldEndTime);
}
public bool IsRunning() {
return Overlaps(DateTime.Now);
}
public override string ToString() {
return type switch {
"1" => "RedAlert",
"2" => "DiscoAlert",
"3" => "DanceOff",
_ => type
};
}
}
} }

191
src/Core/SpecialRoom.cs Normal file
View File

@ -0,0 +1,191 @@
using sodoffmmo.Data;
namespace sodoffmmo.Core;
public class SpecialRoom : Room {
public double[] ambassadorGauges = new double[3]; // There is always a maximum of 3.
System.Timers.Timer? ambassadorTimer;
public static void CreateRooms() {
foreach (var room in Configuration.ServerConfiguration.RoomAlerts) {
foreach (var alert in room.Value) {
AlertInfo alertInfo = new AlertInfo(
alert[0], // type
float.Parse(alert[1], System.Globalization.CultureInfo.InvariantCulture.NumberFormat), // duration
Int32.Parse(alert[2]), Int32.Parse(alert[3]), // start min - max for random start time
Int32.Parse(alert[4]), Int32.Parse(alert[5]) // extra parameters for specific alarm types
);
Console.WriteLine($"Setup alert {alertInfo} for {room.Key}");
(rooms.GetValueOrDefault(room.Key) as SpecialRoom ?? new SpecialRoom(room.Key)).AddAlert(alertInfo);
}
}
foreach (var room in Configuration.ServerConfiguration.AmbassadorRooms) {
Console.WriteLine($"Setup Ambassador for {room}");
(rooms.GetValueOrDefault(room) as SpecialRoom ?? new SpecialRoom(room)).InitAmbassador();
}
}
public SpecialRoom(string name) : base(name) {}
public void InitAmbassador() {
for (int i=0;i<3;i++) ambassadorGauges[i] = Configuration.ServerConfiguration.AmbassadorGaugeStart;
ambassadorTimer = new(Configuration.ServerConfiguration.AmbassadorGaugeDecayRate * 1000) {
AutoReset = true,
Enabled = true
};
ambassadorTimer.Elapsed += (sender, e) => {
if (!Configuration.ServerConfiguration.AmbassadorGaugeDecayOnlyWhenInRoom || ClientsCount > 0) {
for (int i=0;i<3;i++) ambassadorGauges[i] = Math.Max(0, ambassadorGauges[i]-1);
Send(Utils.VlNetworkPacket(GetRoomVars(), Id));
}
};
}
internal override NetworkArray GetRoomVars() {
NetworkArray vars = new();
vars.Add(NetworkArray.VlElement("COUNT", (int)Math.Round(ambassadorGauges[0]), isPersistent: true));
vars.Add(NetworkArray.VlElement("COUNT2", (int)Math.Round(ambassadorGauges[1]), isPersistent: true));
vars.Add(NetworkArray.VlElement("COUNT3", (int)Math.Round(ambassadorGauges[2]), isPersistent: true));
for (int i=0;i<RoomVariables.Length;i++) vars.Add(RoomVariables[i]);
return vars;
}
private int alertId = -1;
private Random random = new();
List<AlertInfo> alerts = new();
public void AddAlert(AlertInfo alert) {
alerts.Add(alert);
ResetAlertTimer(alert);
}
public void SendAllAlerts(Client client) {
return; // Disables joining ongoing alerts (since it doesn't work properly).
foreach (AlertInfo alert in alerts) {
if (alert.IsRunning()) StartAlert(alert, client);
}
}
private void StartAlert(AlertInfo alert, Client? specificClient = null) {
NetworkArray NewRoomVariables = new();
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_START, alertId++, isPersistent: true));
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TYPE, alert.type, isPersistent: true));
double duration = (alert.endTime - DateTime.Now).TotalSeconds;
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_LENGTH, alert.type == "1" ? alert.redAlertDuration : duration, isPersistent: true));
if (alert.type == "1") {
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_TIMEOUT, duration, isPersistent: true));
} else if (alert.type == "3") {
alert.songId = random.Next(0, alert.songs);
NewRoomVariables.Add(NetworkArray.VlElement(REDALERT_SONG, (double)alert.songId, isPersistent: true));
}
NetworkPacket packet = Utils.VlNetworkPacket(NewRoomVariables, Id);
if (specificClient is null) {
RoomVariables = NewRoomVariables;
Send(packet);
RoomVariables = new();
Console.WriteLine("Started event " +alert + " in room " + Name);
} else {
specificClient.Send(packet);
Console.WriteLine("Added " + specificClient.PlayerData.DiplayName + " to event " + alert + " with " + duration + " seconds remaining");
}
}
void ResetAlertTimer(AlertInfo alert) {
System.Timers.Timer? timer = alert.timer;
if (timer != null) {
timer.Stop();
timer.Close();
}
DateTime startTime = DateTime.Now.AddMilliseconds(random.Next(alert.minTime * 1000, alert.maxTime * 1000));
DateTime endTime = startTime.AddSeconds(alert.duration);
for (int i = 0; i < alerts.IndexOf(alert); i++) {
// Prevent overlap between two events.
if (alerts[i].Overlaps(endTime)) {
startTime = alerts[i].endTime.AddSeconds(5);
endTime = startTime.AddSeconds(alert.duration);
}
}
timer = new System.Timers.Timer((startTime - DateTime.Now).TotalMilliseconds);
timer.AutoReset = false;
timer.Enabled = true;
timer.Elapsed += (sender, e) => StartAlert(alert);
timer.Elapsed += (sender, e) => ResetAlertTimer(alert);
alert.timer = timer;
Console.WriteLine("Event " + alert + " in " + Name + " scheduled for " + startTime.ToString("MM/dd/yyyy HH:mm:ss tt") + " (in " + (startTime - DateTime.Now).TotalSeconds + " seconds)");
alert.startTime = startTime;
alert.endTime = endTime;
}
private const string REDALERT_START = "RA_S";
private const string REDALERT_TYPE = "RA_A";
private const string REDALERT_LENGTH = "RA_L";
private const string REDALERT_TIMEOUT = "RA_T";
private const string REDALERT_SONG = "RA_SO";
public class AlertInfo {
public readonly string type;
public readonly double duration;
public readonly int minTime;
public readonly int maxTime;
public readonly int redAlertDuration;
public readonly int songs;
public int songId;
public DateTime startTime {
get {
return newStartTime;
}
set {
oldStartTime = newStartTime;
newStartTime = value;
}
}
public DateTime endTime {
get {
return newEndTime;
}
set {
oldEndTime = newEndTime;
newEndTime = value;
}
}
private DateTime newStartTime;
private DateTime newEndTime;
private DateTime oldStartTime;
private DateTime oldEndTime;
public System.Timers.Timer? timer = null;
public AlertInfo(string type, double duration = 20.0, int minTime = 30, int maxTime = 240, int redAlertDuration = 60, int songs = 16) {
this.type = type;
this.duration = duration;
this.minTime = minTime;
this.maxTime = maxTime;
this.redAlertDuration = redAlertDuration;
this.songs = songs;
}
public bool Overlaps(DateTime time) {
return (time >= oldStartTime && time <= oldEndTime);
}
public bool IsRunning() {
return Overlaps(DateTime.Now);
}
public override string ToString() {
return type switch {
"1" => "RedAlert",
"2" => "DiscoAlert",
"3" => "DanceOff",
_ => type
};
}
}
}

10
src/Core/UserBanType.cs Normal file
View File

@ -0,0 +1,10 @@
namespace sodoffmmo.Core;
public enum UserBanType
{
NotBanned = 0,
IndefiniteOpenChatBan = 1,
TemporaryOpenChatBan = 2,
IndefiniteAccountBan = 3,
TemporaryAccountBan = 4
}

View File

@ -38,6 +38,8 @@ public class PlayerData {
public string DiplayName { get; set; } = "placeholder"; public string DiplayName { get; set; } = "placeholder";
public Role Role { get; set; } = Role.User; public Role Role { get; set; } = Role.User;
public long last_ue_time { get; set; } = 0;
public static readonly string[] SupportedVariables = { public static readonly string[] SupportedVariables = {
"A", // avatar data "A", // avatar data
"FP", // raised pet data "FP", // raised pet data
@ -57,6 +59,9 @@ public class PlayerData {
"P", // position vector (older games) "P", // position vector (older games)
"R", // rotation (older games - updated via SUV, not SPV) "R", // rotation (older games - updated via SUV, not SPV)
"F", // flags (older games - updated via SUV, not SPV) "F", // flags (older games - updated via SUV, not SPV)
"UTI", // group/clan
"SPM", // drive mode (Eat My Dust)
"H", // health
}; };
// other variables (set and updated via SUV command) // other variables (set and updated via SUV command)
@ -66,18 +71,18 @@ public class PlayerData {
return variables[varName]; return variables[varName];
} }
public void SetVariable(string varName, string value) { public string SetVariable(string varName, string value) {
// do not store in variables directory // do not store in variables directory
if (varName == "UID") { if (varName == "UID") {
return; return value;
} }
if (varName == "R") { if (varName == "R") {
R = float.Parse(value, CultureInfo.InvariantCulture); R = float.Parse(value, CultureInfo.InvariantCulture);
return; return value;
} }
if (varName == "F") { if (varName == "F") {
F = unchecked((int)Convert.ToUInt32(value, 16)); F = unchecked((int)Convert.ToUInt32(value, 16));
return; return value;
} }
// fix variable value before store // fix variable value before store
@ -87,11 +92,13 @@ public class PlayerData {
// store in directory // store in directory
variables[varName] = value; variables[varName] = value;
return value;
} }
public void InitFromNetworkData(NetworkObject suvData) { public void InitFromNetworkData(NetworkObject suvData) {
// set initial state for SPV data // set initial state for SPV data
R = float.Parse(suvData.Get<string>("R"), CultureInfo.InvariantCulture); string? r = suvData.Get<string>("R"); // in Eat My Dust, rotation is sent in R1, R2, R3
if (r != null) R = float.Parse(r, CultureInfo.InvariantCulture);
string? p1 = suvData.Get<string>("P1"); string? p1 = suvData.Get<string>("P1");
if (p1 != null) { if (p1 != null) {
P1 = float.Parse(p1, CultureInfo.InvariantCulture); P1 = float.Parse(p1, CultureInfo.InvariantCulture);

View File

@ -0,0 +1,26 @@
using System;
using System.Net.Http.Json;
using sodoffmmo.Attributes;
using sodoffmmo.Core;
namespace sodoffmmo.Management.Commands;
[ManagementCommand("ban", Role.Moderator)]
class BanCommand : IManagementCommand
{
public void Handle(Client client, string[] arguments)
{
if(arguments.Length < 2) { client.Send(Utils.BuildServerSideMessage($"Expected 3 Args, Got {arguments.Length + 1}", "Server")); return; }
if (arguments[0] == "help") client.Send(Utils.BuildServerSideMessage($"::ban - This bans a user who is in-room. First argument is the id of the user in-room. Room user lists start at 0.", "Server"));
var clientToBan = client.Room!.Clients.ToArray()[int.Parse(arguments[0])].PlayerData.Uid;
if (clientToBan == null) { client.Send(Utils.BuildServerSideMessage($"User Could Not Be Found", "Server")); return; }
// send an http request to the api set in appsettings
ApiWebService apiWebService = new();
var response = apiWebService.BanUser(client, clientToBan, arguments[1], arguments[2]);
if (response != null) { client.Send(Utils.BuildServerSideMessage("User Banned Successfully", "Server")); return; }
else { client.Send(Utils.BuildServerSideMessage("Empty Response", "Server")); }
}
}

View File

@ -12,11 +12,14 @@ public class Server {
readonly IPAddress ipAddress; readonly IPAddress ipAddress;
readonly bool IPv6AndIPv4; readonly bool IPv6AndIPv4;
ModuleManager moduleManager = new(); ModuleManager moduleManager = new();
public static List<Client> AllClients { get; private set; }
public Server(IPAddress ipAdress, int port, bool IPv6AndIPv4) { public Server(IPAddress ipAdress, int port, bool IPv6AndIPv4) {
this.ipAddress = ipAdress; this.ipAddress = ipAdress;
this.port = port; this.port = port;
this.IPv6AndIPv4 = IPv6AndIPv4; this.IPv6AndIPv4 = IPv6AndIPv4;
AllClients = new List<Client>();
} }
public async Task Run() { public async Task Run() {
@ -29,17 +32,7 @@ public class Server {
listener.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0); listener.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
listener.Bind(new IPEndPoint(ipAddress, port)); listener.Bind(new IPEndPoint(ipAddress, port));
foreach (var room in Configuration.ServerConfiguration.RoomAlerts) { SpecialRoom.CreateRooms();
foreach (var alert in room.Value) {
Console.WriteLine($"Setup alert \"{alert[0]}\" for {room.Key}");
Room.GetOrAdd(room.Key).AddAlert(new Room.AlertInfo(
alert[0], // type
float.Parse(alert[1], System.Globalization.CultureInfo.InvariantCulture.NumberFormat), // duration
Int32.Parse(alert[2]), Int32.Parse(alert[3]), // start min - max for random start time
Int32.Parse(alert[4]), Int32.Parse(alert[5]) // extra parameters for specific alarm types
));
}
}
await Listen(listener); await Listen(listener);
} }
@ -57,6 +50,7 @@ public class Server {
private async Task HandleClient(Socket handler) { private async Task HandleClient(Socket handler) {
Client client = new(handler); Client client = new(handler);
AllClients.Add(client);
try { try {
while (client.Connected) { while (client.Connected) {
await client.Receive(); await client.Receive();
@ -69,6 +63,7 @@ public class Server {
} finally { } finally {
try { try {
client.SetRoom(null); client.SetRoom(null);
AllClients.Remove(client);
} catch (Exception) { } } catch (Exception) { }
client.Disconnect(); client.Disconnect();
Console.WriteLine("Socket disconnected IID: " + client.ClientID); Console.WriteLine("Socket disconnected IID: " + client.ClientID);

View File

@ -23,13 +23,33 @@
"// RoomAlerts": "List of MMO rooms with alert function. Default empty (not used by SoD), bellow sample config for WoJS, MB and SS.", "// RoomAlerts": "List of MMO rooms with alert function. Default empty (not used by SoD), bellow sample config for WoJS, MB and SS.",
"// alert parameters": "alert type, duration [s], minimum time to start [s], maximum time to start [s], redAlertDuration (used for type '1'), number of songs (used for type '3')", "// alert parameters": "alert type, duration [s], minimum time to start [s], maximum time to start [s], redAlertDuration (used for type '1'), number of songs (used for type '3')",
"// alert types": "1 - Red Alert, 2 - Disco Alert, 3 - Dance Off",
"RoomAlerts": { "RoomAlerts": {
"LoungeInt" : [ ["3", 20.0, 30, 240, 0, 16] ], "LoungeInt": [ [ "3", 20.0, 30, 240, 0, 16 ] ],
"Spaceport": [ ["1", 20.0, 300, 300, 60, 0], ["2", 120.0, 1800, 3600, 60, 0] ], "Spaceport": [
"Academy": [ ["1", 20.0, 300, 300, 60, 0] ], [ "1", 20.0, 300, 300, 60, 0 ],
"ClubSSInt" : [ ["3", 20.0, 30, 240, 0, 16] ], [ "2", 120.0, 1800, 3600, 60, 0 ]
],
"Academy": [ [ "1", 20.0, 300, 300, 60, 0 ] ],
"ClubSSInt": [ [ "3", 20.0, 30, 240, 0, 16 ] ],
"JunkYardEMD": [ [ "1", 20.0, 240, 300, 60, 0 ] ]
}, },
"// AmbassadorRooms": "Rooms with ambassadors (MB funzones).",
"AmbassadorRooms": [ "Spaceport" ],
"// AmbassadorGaugeStart": "The starting value for all ambassador gauges (MB funzones).",
"AmbassadorGaugeStart": 75,
"// AmbassadorGaugeDecayRate": "Time in seconds before ambassador gauges decrease (MB funzones).",
"AmbassadorGaugeDecayRate": 60,
"// AmbassadorGaugeDecayOnlyWhenInRoom": "Only decrease ambassador gauges when there is at least one player in the room (MB funzones).",
"AmbassadorGaugeDecayOnlyWhenInRoom": true,
"// AmbassadorGaugePlayers": "Denominator for filling the ambassador gauges (MB funzones).",
"AmbassadorGaugePlayers": 0.5,
"// RacingMaxPlayers": "maximum players allowed in Thunder Run Racing (no more than 6)", "// RacingMaxPlayers": "maximum players allowed in Thunder Run Racing (no more than 6)",
"RacingMaxPlayers": 6, "RacingMaxPlayers": 6,
@ -44,9 +64,9 @@
"// Authentication Optional": "authentication is required only for moderation activities", "// Authentication Optional": "authentication is required only for moderation activities",
"// Authentication RequiredForChat": "authentication is required only for moderation activities and using chat (if chat is enabled)", "// Authentication RequiredForChat": "authentication is required only for moderation activities and using chat (if chat is enabled)",
"// Authentication Required": "authentication is required to connect to mmo", "// Authentication Required": "authentication is required to connect to mmo",
"Authentication": "Disabled", "Authentication": "RequiredForChat",
"// ApiUrl": "SoDOff API server URL for authentication calls", "// ApiUrl": "SoDOff API server URL for authentication calls and other calls",
"ApiUrl": "http://localhost:5000", "ApiUrl": "http://localhost:5000",
"// BypassToken": "Token allowed to connect without authentication", "// BypassToken": "Token allowed to connect without authentication",