server-master/srcs/_plugins/WingsEmu.Plugins.Essentials/Administrator/AdministratorMaintenanceModule.cs
2026-02-10 18:21:30 +01:00

311 lines
No EOL
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using PhoenixLib.Logging;
using Qmmands;
using WingsAPI.Communication;
using WingsAPI.Communication.Player;
using WingsAPI.Communication.ServerApi;
using WingsAPI.Communication.ServerApi.Protocol;
using WingsAPI.Communication.Services;
using WingsAPI.Communication.Services.Requests;
using WingsAPI.Communication.Services.Responses;
using WingsEmu.Commands.Checks;
using WingsEmu.Commands.Entities;
using WingsEmu.DTOs.Account;
using WingsEmu.Game.Extensions;
using WingsEmu.Game.Features;
using WingsEmu.Game.Networking;
namespace WingsEmu.Plugins.Essentials.Administrator;
[Name("Maintenance")]
[Description("Module related to maintenance admin commands.")]
[Group("status", "maintenance")]
[RequireAuthority(AuthorityType.Owner)]
public class AdministratorMaintenanceModule : SaltyModuleBase
{
private readonly IClusterCharacterService _characterService;
private readonly IClusterStatusService _service;
public AdministratorMaintenanceModule(IClusterStatusService service, IClusterCharacterService characterService)
{
_service = service;
_characterService = characterService;
}
[Command("show", "count", "players", "channels")]
[Description("Shows amount of players per channel")]
public async Task<SaltyCommandResult> ShowStats()
{
ClusterCharacterGetSortedResponse response = null;
try
{
response = await _characterService.GetCharactersSortedByChannel(new EmptyRpcRequest());
}
catch (Exception e)
{
Log.Error("[MAINTENANCE_MODULE][SHOW_STATS] Unexpected error: ", e);
}
if (response?.ResponseType != RpcResponseType.SUCCESS || response.CharactersByChannel == null)
{
return new SaltyCommandResult(false, "Couldn't obtain the information from ClusterCharacterService.");
}
Context.Player.SendInformationChatMessage("[======== CLUSTER =========]");
foreach ((byte channelId, List<ClusterCharacterInfo> characterInfos) in response.CharactersByChannel)
{
Context.Player.SendInformationChatMessage($"Channel {channelId.ToString()}: {(characterInfos?.Count ?? 0).ToString()} players");
}
return new SaltyCommandResult(true);
}
[Command("schedule", "start")]
[Description("Schedules a general maintenance")]
public async Task<SaltyCommandResult> ScheduleMaintenance([Description("The maintenance delay")] string inputTimeSpan, [Description("Reason of the maintenance")] params string[] reason)
{
if (!TimeSpan.TryParse(inputTimeSpan, out TimeSpan timeSpan))
{
return new SaltyCommandResult(false, "Failed to parse the introduced delay");
}
StringBuilder stringBuilder = new();
foreach (string substring in reason)
{
stringBuilder.Append(' ');
stringBuilder.Append(substring);
}
BasicRpcResponse response;
try
{
response = await _service.ScheduleGeneralMaintenance(new ServiceScheduleGeneralMaintenanceRequest
{
ShutdownTimeSpan = timeSpan,
Reason = stringBuilder.ToString().TrimStart()
});
}
catch (Exception e)
{
Log.Error("[MAINTENANCE_MODULE][SCHEDULE_MAINTENANCE]", e);
return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master");
}
return response.ResponseType != RpcResponseType.SUCCESS
? new SaltyCommandResult(false, "Cluster/Master denied the general maintenance")
: new SaltyCommandResult(true, "A general maintenance has been scheduled!");
}
[Command("unschedule", "stop", "cancel")]
[Description("Unschedules a general maintenance")]
public async Task<SaltyCommandResult> UnscheduleMaintenance()
{
BasicRpcResponse response;
try
{
response = await _service.UnscheduleGeneralMaintenance(new EmptyRpcRequest());
}
catch (Exception e)
{
Log.Error("[MAINTENANCE_MODULE][UNSCHEDULE_MAINTENANCE]", e);
return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master");
}
return response.ResponseType != RpcResponseType.SUCCESS
? new SaltyCommandResult(false, "Cluster/Master denied the unschedule of the maintenance")
: new SaltyCommandResult(true, "The maintenance has been unscheduled!");
}
[Command("emergency")]
[Description("Executes an emergency general maintenance")]
public async Task<SaltyCommandResult> ExecuteEmergencyMaintenance([Description("Reason of the maintenance")] params string[] reason)
{
StringBuilder stringBuilder = new();
foreach (string substring in reason)
{
stringBuilder.Append(' ');
stringBuilder.Append(substring);
}
BasicRpcResponse response;
try
{
response = await _service.ExecuteGeneralEmergencyMaintenance(new ServiceExecuteGeneralEmergencyMaintenanceRequest
{
Reason = stringBuilder.ToString().TrimStart()
});
}
catch (Exception e)
{
Log.Error("[MAINTENANCE_MODULE][EXECUTE_EMERGENCY_MAINTENANCE]", e);
return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master");
}
return response.ResponseType != RpcResponseType.SUCCESS
? new SaltyCommandResult(false, "Cluster/Master denied the emergency maintenance")
: new SaltyCommandResult(true, "The Emergency Maintenance has been executed!");
}
[Command("lift")]
[Description("Lifts a general maintenance (even emergency ones)")]
public async Task<SaltyCommandResult> LiftMaintenance()
{
BasicRpcResponse response;
try
{
response = await _service.LiftGeneralMaintenance(new EmptyRpcRequest());
}
catch (Exception e)
{
Log.Error("[MAINTENANCE_MODULE][LIFT_MAINTENANCE]", e);
return new SaltyCommandResult(false, "Couldn't connect with the Cluster/Master");
}
return response.ResponseType != RpcResponseType.SUCCESS
? new SaltyCommandResult(false, "Cluster/Master denied the maintenance lift")
: new SaltyCommandResult(true, "The Maintenance has been lifted!");
}
[Group("feature")]
[Description("Sub-module related to individual management of features")]
public class AdministratorMaintenanceFeatureModule : SaltyModuleBase
{
private readonly IGameFeatureToggleManager _gameFeatureToggleManager;
public AdministratorMaintenanceFeatureModule(IGameFeatureToggleManager gameFeatureToggleManager) => _gameFeatureToggleManager = gameFeatureToggleManager;
[Command("disable")]
[Description("Disable a enabled feature")]
public async Task<SaltyCommandResult> DisableFeature([Description("Name of the feature")] GameFeature feature)
{
bool disabled = await _gameFeatureToggleManager.IsDisabled(feature);
if (disabled)
{
return new SaltyCommandResult(false, "This feature is already disabled");
}
await _gameFeatureToggleManager.Disable(feature);
return new SaltyCommandResult(true, $"Successfully disabled: {feature}");
}
[Command("enable")]
[Description("Enable a game disabled feature")]
public async Task<SaltyCommandResult> EnableFeature([Description("Name of the feature")] GameFeature feature)
{
bool disabled = await _gameFeatureToggleManager.IsDisabled(feature);
if (!disabled)
{
return new SaltyCommandResult(false, "This feature is already enabled");
}
await _gameFeatureToggleManager.Enable(feature);
return new SaltyCommandResult(true, $"Successfully enabled: {feature}");
}
[Command("list")]
[Description("List all game feature status")]
public async Task<SaltyCommandResult> List()
{
Context.Player.SendInformationChatMessage("[========= GAME FEATURES STATUS =========]");
foreach (GameFeature feature in Enum.GetValues<GameFeature>())
{
bool disabled = await _gameFeatureToggleManager.IsDisabled(feature);
Context.Player.SendInformationChatMessage($"{feature}: {(!disabled ? "ON" : "OFF")}");
}
Context.Player.SendInformationChatMessage("[========================================]");
return new SaltyCommandResult(true);
}
}
[Group("service")]
[Description("Sub-Module related to individual management of services.")]
public class AdministratorMaintenanceServiceModule : SaltyModuleBase
{
private readonly IClusterStatusService _service;
public AdministratorMaintenanceServiceModule(IClusterStatusService service) => _service = service;
[Command("list")]
[Description("Lists the status of the cluster services")]
public async Task<SaltyCommandResult> ListMaintenanceAsync()
{
IClientSession session = Context.Player;
ServiceGetAllResponse resp = await _service.GetAllServicesStatus(new EmptyRpcRequest());
session.SendInformationChatMessage("[========= CLUSTER STATUS =========]");
foreach (Service service in resp.Services)
{
session.SendInformationChatMessage($"[{service.Id}] {service.Status.ToString()} - {service.LastUpdate:yyyy-MM-dd HH:mm:ss}");
}
session.SendInformationChatMessage("[=============================]");
return new SaltyCommandResult(true, "Services listed");
}
[Command("set", "enable", "activate")]
[Description("Sets a designated service in maintenance mode")]
public async Task<SaltyCommandResult> EnableMaintenanceMode(string serviceName)
{
BasicRpcResponse resp = await _service.EnableMaintenanceMode(new ServiceBasicRequest
{
ServiceName = serviceName
});
if (resp.ResponseType != RpcResponseType.SUCCESS)
{
return new SaltyCommandResult(false, $"{serviceName} could not be put in maintenance mode");
}
return new SaltyCommandResult(true, $"{serviceName} is now in maintenance mode");
}
[Command("unset", "disable", "deactivate")]
[Description("Sets a designated service in maintenance mode")]
public async Task<SaltyCommandResult> DisableMaintenanceMode(string serviceName)
{
BasicRpcResponse resp = await _service.DisableMaintenanceMode(new ServiceBasicRequest
{
ServiceName = serviceName
});
if (resp.ResponseType != RpcResponseType.SUCCESS)
{
return new SaltyCommandResult(false, $"{serviceName} could not be removed from maintenance mode");
}
return new SaltyCommandResult(true, $"{serviceName} is not in maintenance mode anymore");
}
}
[Group("channel-list", "cl")]
[Description("Sub-Module related to management of the Login's Channel List.")]
public class AdministratorMaintenanceChannelListModule : SaltyModuleBase
{
private readonly IServerApiService _serverApiService;
public AdministratorMaintenanceChannelListModule(IServerApiService serverApiService) => _serverApiService = serverApiService;
[Command("set")]
[Description("Sets a designated channel's visibility")]
public async Task<SaltyCommandResult> SetVisibility(int channelId, string worldGroup = "NosWings", AuthorityType authorityType = AuthorityType.User)
{
BasicRpcResponse resp = await _serverApiService.SetWorldServerVisibility(new SetWorldServerVisibilityRequest
{
ChannelId = channelId,
WorldGroup = worldGroup,
AuthorityRequired = authorityType
});
if (resp.ResponseType != RpcResponseType.SUCCESS)
{
return new SaltyCommandResult(false, $"[{channelId.ToString()}:{worldGroup}] Failed to set visibility");
}
return new SaltyCommandResult(true, $"[{channelId.ToString()}:{worldGroup}] Visibility set to '{authorityType.ToString()}'");
}
}
}