diff --git a/GameServer/Controllers/Attributes/ChatCommandAttribute.cs b/GameServer/Controllers/Attributes/ChatCommandAttribute.cs new file mode 100644 index 0000000..0e9ce23 --- /dev/null +++ b/GameServer/Controllers/Attributes/ChatCommandAttribute.cs @@ -0,0 +1,12 @@ +namespace GameServer.Controllers.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +internal class ChatCommandAttribute : Attribute +{ + public string Command { get; } + + public ChatCommandAttribute(string command) + { + Command = command; + } +} diff --git a/GameServer/Controllers/Attributes/ChatCommandCategoryAttribute.cs b/GameServer/Controllers/Attributes/ChatCommandCategoryAttribute.cs new file mode 100644 index 0000000..e6f69a8 --- /dev/null +++ b/GameServer/Controllers/Attributes/ChatCommandCategoryAttribute.cs @@ -0,0 +1,12 @@ +namespace GameServer.Controllers.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +internal class ChatCommandCategoryAttribute : Attribute +{ + public string Category { get; } + + public ChatCommandCategoryAttribute(string category) + { + Category = category; + } +} diff --git a/GameServer/Controllers/Attributes/ChatCommandDescAttribute.cs b/GameServer/Controllers/Attributes/ChatCommandDescAttribute.cs new file mode 100644 index 0000000..71da962 --- /dev/null +++ b/GameServer/Controllers/Attributes/ChatCommandDescAttribute.cs @@ -0,0 +1,12 @@ +namespace GameServer.Controllers.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +internal class ChatCommandDescAttribute : Attribute +{ + public string Description { get; } + + public ChatCommandDescAttribute(string description) + { + Description = description; + } +} diff --git a/GameServer/Controllers/ChatCommands/ChatCommandManager.cs b/GameServer/Controllers/ChatCommands/ChatCommandManager.cs new file mode 100644 index 0000000..fc02d4a --- /dev/null +++ b/GameServer/Controllers/ChatCommands/ChatCommandManager.cs @@ -0,0 +1,77 @@ +using System.Collections.Immutable; +using System.Linq.Expressions; +using System.Reflection; +using GameServer.Controllers.Attributes; +using Microsoft.Extensions.DependencyInjection; + +namespace GameServer.Controllers.ChatCommands; +internal class ChatCommandManager +{ + private delegate Task ChatCommandDelegate(IServiceProvider serviceProvider, string[] args); + private static readonly ImmutableDictionary> s_commandCategories; + private static readonly ImmutableArray s_commandDescriptions; + + private readonly IServiceProvider _serviceProvider; + + static ChatCommandManager() + { + (s_commandCategories, s_commandDescriptions) = RegisterCommands(); + } + + public static IEnumerable CommandDescriptions => s_commandDescriptions; + + public ChatCommandManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task InvokeCommandAsync(string category, string command, string[] args) + { + if (s_commandCategories.TryGetValue(category, out var commands)) + { + if (commands.TryGetValue(command, out ChatCommandDelegate? commandDelegate)) + await commandDelegate(_serviceProvider, args); + } + } + + private static (ImmutableDictionary>, ImmutableArray) RegisterCommands() + { + IEnumerable types = Assembly.GetExecutingAssembly().GetTypes() + .Where(type => type.GetCustomAttribute() != null); + + MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), [typeof(IServiceProvider)])!; + var categories = ImmutableDictionary.CreateBuilder>(); + var descriptions = ImmutableArray.CreateBuilder(); + + foreach (Type type in types) + { + var commands = ImmutableDictionary.CreateBuilder(); + foreach (MethodInfo method in type.GetMethods()) + { + ChatCommandAttribute? cmdAttribute = method.GetCustomAttribute(); + if (cmdAttribute == null) continue; + + ParameterExpression serviceProviderParam = Expression.Parameter(typeof(IServiceProvider)); + ParameterExpression argsParam = Expression.Parameter(typeof(string[])); + + MethodCallExpression getServiceCall = Expression.Call(getServiceMethod.MakeGenericMethod(type), serviceProviderParam); + Expression handlerCall = Expression.Call(getServiceCall, method, argsParam); + + if (method.ReturnType == typeof(void)) // Allow non-async methods as well + handlerCall = Expression.Block(handlerCall, Expression.Constant(Task.CompletedTask)); + + Expression lambda = Expression.Lambda(handlerCall, serviceProviderParam, argsParam); + commands.Add(cmdAttribute.Command, lambda.Compile()); + + ChatCommandDescAttribute? desc = method.GetCustomAttribute(); + if (desc != null) + descriptions.Add(desc.Description); + } + + ChatCommandCategoryAttribute categoryAttribute = type.GetCustomAttribute()!; + categories.Add(categoryAttribute.Category, commands.ToImmutable()); + } + + return (categories.ToImmutable(), descriptions.ToImmutable()); + } +} diff --git a/GameServer/Controllers/ChatCommands/ChatSpawnCommandHandler.cs b/GameServer/Controllers/ChatCommands/ChatSpawnCommandHandler.cs new file mode 100644 index 0000000..2a9fe7e --- /dev/null +++ b/GameServer/Controllers/ChatCommands/ChatSpawnCommandHandler.cs @@ -0,0 +1,62 @@ +using Core.Config; +using GameServer.Controllers.Attributes; +using GameServer.Models; +using GameServer.Models.Chat; +using GameServer.Network; +using GameServer.Systems.Entity; +using Protocol; + +namespace GameServer.Controllers.ChatCommands; + +[ChatCommandCategory("spawn")] +internal class ChatSpawnCommandHandler +{ + private readonly ChatRoom _helperRoom; + private readonly EntitySystem _entitySystem; + private readonly EntityFactory _entityFactory; + private readonly PlayerSession _session; + private readonly ConfigManager _configManager; + + public ChatSpawnCommandHandler(ModelManager modelManager, EntitySystem entitySystem, EntityFactory entityFactory, PlayerSession session, ConfigManager configManager) + { + _helperRoom = modelManager.Chat.GetChatRoom(1338); + _entitySystem = entitySystem; + _entityFactory = entityFactory; + _session = session; + _configManager = configManager; + } + + [ChatCommand("monster")] + [ChatCommandDesc("/spawn monster [id] [x] [y] [z] - spawns monster with specified id and coordinates")] + public async Task OnSpawnMonsterCommand(string[] args) + { + if (args.Length != 4 || + !(int.TryParse(args[0], out int levelEntityId) && + int.TryParse(args[1], out int x) && + int.TryParse(args[2], out int y) && + int.TryParse(args[3], out int z))) + { + _helperRoom.AddMessage(1338, 0, "Usage: /spawn monster [id] [x] [y] [z]"); + return; + } + + MonsterEntity monster = _entityFactory.CreateMonster(levelEntityId); + monster.Pos = new() + { + X = x, + Y = y, + Z = z + }; + + _entitySystem.Create(monster); + monster.InitProps(_configManager.GetConfig(600000100)!); // TODO: monster property config + + await _session.Push(MessageId.EntityAddNotify, new EntityAddNotify + { + IsAdd = true, + EntityPbs = { monster.Pb } + }); + + _helperRoom.AddMessage(1338, 0, $"Successfully spawned monster with id {levelEntityId} at ({x}, {y}, {z})"); + } +} diff --git a/GameServer/Controllers/ChatController.cs b/GameServer/Controllers/ChatController.cs new file mode 100644 index 0000000..31a52ae --- /dev/null +++ b/GameServer/Controllers/ChatController.cs @@ -0,0 +1,88 @@ +using System.Text; +using GameServer.Controllers.Attributes; +using GameServer.Controllers.ChatCommands; +using GameServer.Models; +using GameServer.Models.Chat; +using GameServer.Network; +using GameServer.Network.Messages; +using Protocol; + +namespace GameServer.Controllers; +internal class ChatController : Controller +{ + private readonly ModelManager _modelManager; + + public ChatController(PlayerSession session, ModelManager modelManager) : base(session) + { + _modelManager = modelManager; + } + + [NetEvent(MessageId.PrivateChatDataRequest)] + public async Task OnPrivateChatDataRequest() + { + if (!_modelManager.Chat.AllChatRooms.Any()) + { + ChatRoom chatRoom = _modelManager.Chat.GetChatRoom(1338); // Reversed Helper + chatRoom.AddMessage(1338, (int)ChatContentType.Text, BuildWelcomeMessage()); + } + + await PushPrivateChatHistory(); + return Response(MessageId.PrivateChatDataResponse, new PrivateChatDataResponse()); // Response doesn't contain any useful info, everything is in notifies. + } + + [NetEvent(MessageId.PrivateChatRequest)] + public async Task OnPrivateChatRequest(PrivateChatRequest request, ChatCommandManager chatCommandManager) + { + ChatRoom chatRoom = _modelManager.Chat.GetChatRoom(1338); + + chatRoom.AddMessage(_modelManager.Player.Id, request.ChatContentType, request.Content); + if (!request.Content.StartsWith('/')) + { + chatRoom.AddMessage(1338, 0, "huh?"); + } + else + { + string[] split = request.Content[1..].Split(' '); + if (split.Length >= 2) + { + await chatCommandManager.InvokeCommandAsync(split[0], split[1], split[2..]); + } + } + + await PushPrivateChatHistory(); + return Response(MessageId.PrivateChatResponse, new PrivateChatResponse()); + } + + [NetEvent(MessageId.PrivateChatOperateRequest)] + public ResponseMessage OnPrivateChatOperateRequest() => Response(MessageId.PrivateChatOperateResponse, new PrivateChatOperateResponse()); + + private async Task PushPrivateChatHistory() + { + await Session.Push(MessageId.PrivateChatHistoryNotify, new PrivateChatHistoryNotify + { + AllChats = + { + _modelManager.Chat.AllChatRooms + .Select(chatRoom => new PrivateChatHistoryContentProto + { + TargetUid = chatRoom.TargetUid, + Chats = { chatRoom.ChatHistory } + }) + } + }); + } + + private static string BuildWelcomeMessage() + { + StringBuilder builder = new(); + builder.AppendLine("Welcome to ReversedRooms WutheringWaves private server!\nTo get support, join:\ndiscord.gg/reversedrooms"); + builder.AppendLine("List of all commands:"); + + foreach (string description in ChatCommandManager.CommandDescriptions) + { + builder.AppendLine(description); + } + + return builder.ToString(); + } +} diff --git a/GameServer/Controllers/CreatureController.cs b/GameServer/Controllers/CreatureController.cs index 4a17c35..1ce646f 100644 --- a/GameServer/Controllers/CreatureController.cs +++ b/GameServer/Controllers/CreatureController.cs @@ -28,6 +28,7 @@ internal class CreatureController : Controller { _modelManager.Creature.SetSceneLoadingData(instanceId); CreateTeamPlayerEntities(); + CreateWorldEntities(); await Session.Push(MessageId.JoinSceneNotify, new JoinSceneNotify { @@ -150,6 +151,13 @@ internal class CreatureController : Controller .FirstOrDefault(e => e is PlayerEntity playerEntity && playerEntity.ConfigId == roleId && playerEntity.PlayerId == _modelManager.Player.Id) as PlayerEntity; } + public IEnumerable GetPlayerEntities() + { + return _entitySystem.EnumerateEntities() + .Where(e => e is PlayerEntity entity && entity.PlayerId == _modelManager.Player.Id) + .Cast(); + } + public async Task SwitchPlayerEntity(int roleId) { PlayerEntity? prevEntity = GetPlayerEntity(); @@ -235,7 +243,10 @@ internal class CreatureController : Controller if (i == 0) _modelManager.Creature.PlayerEntityId = entity.Id; } + } + private void CreateWorldEntities() + { // Test monster MonsterEntity monster = _entityFactory.CreateMonster(102000014); // Monster001 monster.Pos = new() diff --git a/GameServer/Controllers/FriendSystemController.cs b/GameServer/Controllers/FriendSystemController.cs index 681fc03..c83fa56 100644 --- a/GameServer/Controllers/FriendSystemController.cs +++ b/GameServer/Controllers/FriendSystemController.cs @@ -12,5 +12,25 @@ internal class FriendSystemController : Controller } [NetEvent(MessageId.FriendAllRequest)] - public ResponseMessage OnFriendAllRequest() => Response(MessageId.FriendAllResponse, new FriendAllResponse()); + public ResponseMessage OnFriendAllRequest() => Response(MessageId.FriendAllResponse, new FriendAllResponse + { + FriendInfoList = + { + CreateDummyFriendInfo(1338, "Taoqi", "discord.gg/reversedrooms", 1601) + } + }); + + private static FriendInfo CreateDummyFriendInfo(int id, string name, string signature, int headIconId) => new() + { + Info = new() + { + PlayerId = id, + Name = name, + Signature = signature, + Level = 5, + HeadId = headIconId, + IsOnline = true, + LastOfflineTime = -1 + } + }; } diff --git a/GameServer/Controllers/PlayerInfoController.cs b/GameServer/Controllers/PlayerInfoController.cs index b95252f..54f8479 100644 --- a/GameServer/Controllers/PlayerInfoController.cs +++ b/GameServer/Controllers/PlayerInfoController.cs @@ -1,6 +1,7 @@ using GameServer.Controllers.Attributes; using GameServer.Models; using GameServer.Network; +using GameServer.Network.Messages; using GameServer.Systems.Event; using Protocol; @@ -56,4 +57,22 @@ internal class PlayerInfoController : Controller await Session.Push(MessageId.BasicInfoNotify, basicInfo); } + + [NetEvent(MessageId.PlayerBasicInfoGetRequest)] + public ResponseMessage OnPlayerBasicInfoGetRequest() + { + return Response(MessageId.PlayerBasicInfoGetResponse, new PlayerBasicInfoGetResponse + { + Info = new PlayerDetails + { + Name = "Taoqi", + Signature = "discord.gg/reversedrooms", + HeadId = 1601, + PlayerId = 1338, + IsOnline = true, + LastOfflineTime = -1, + Level = 5 + } + }); + } } diff --git a/GameServer/Extensions/ServiceCollectionExtensions.cs b/GameServer/Extensions/ServiceCollectionExtensions.cs index 1904afc..3573550 100644 --- a/GameServer/Extensions/ServiceCollectionExtensions.cs +++ b/GameServer/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using System.Reflection; using GameServer.Controllers; +using GameServer.Controllers.Attributes; using Microsoft.Extensions.DependencyInjection; namespace GameServer.Extensions; @@ -17,4 +18,17 @@ internal static class ServiceCollectionExtensions return services; } + + public static IServiceCollection AddCommands(this IServiceCollection services) + { + IEnumerable handlerTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null); + + foreach (Type type in handlerTypes) + { + services.AddScoped(type); + } + + return services; + } } diff --git a/GameServer/GameServer.csproj b/GameServer/GameServer.csproj index 3d4e753..619bef0 100644 --- a/GameServer/GameServer.csproj +++ b/GameServer/GameServer.csproj @@ -5,8 +5,13 @@ net8.0 enable enable + icon.ico + + + + diff --git a/GameServer/Models/Chat/ChatRoom.cs b/GameServer/Models/Chat/ChatRoom.cs new file mode 100644 index 0000000..785e924 --- /dev/null +++ b/GameServer/Models/Chat/ChatRoom.cs @@ -0,0 +1,32 @@ +using Google.Protobuf; +using Protocol; + +namespace GameServer.Models.Chat; +internal class ChatRoom +{ + private readonly List _messages; + private int _msgIdCounter; + + public int TargetUid { get; } + + public ChatRoom(int targetUid) + { + TargetUid = targetUid; + _messages = []; + } + + public IEnumerable ChatHistory => _messages; + + public void AddMessage(int senderId, int contentType, string content) + { + _messages.Add(new ChatContentProto + { + SenderUid = senderId, + ChatContentType = contentType, + Content = content, + MsgId = NextMessageId().ToString() + }); + } + + private int NextMessageId() => Interlocked.Increment(ref _msgIdCounter); +} diff --git a/GameServer/Models/ChatModel.cs b/GameServer/Models/ChatModel.cs new file mode 100644 index 0000000..063a1bd --- /dev/null +++ b/GameServer/Models/ChatModel.cs @@ -0,0 +1,29 @@ +using GameServer.Models.Chat; + +namespace GameServer.Models; +internal class ChatModel +{ + private readonly Dictionary _rooms; + + public ChatModel() + { + _rooms = []; + } + + /// + /// Gets chat room for specified player id. + /// Creates new one if it doesn't exist. + /// + public ChatRoom GetChatRoom(int id) + { + if (!_rooms.TryGetValue(id, out ChatRoom? chatRoom)) + { + chatRoom = new ChatRoom(id); + _rooms[id] = chatRoom; + } + + return chatRoom; + } + + public IEnumerable AllChatRooms => _rooms.Values; +} diff --git a/GameServer/Models/ModelManager.cs b/GameServer/Models/ModelManager.cs index 95e0631..7b82510 100644 --- a/GameServer/Models/ModelManager.cs +++ b/GameServer/Models/ModelManager.cs @@ -30,4 +30,5 @@ internal class ModelManager public CreatureModel Creature => _creatureModel ?? throw new InvalidOperationException($"Trying to access {nameof(CreatureModel)} instance before initialization!"); public FormationModel Formation { get; } = new(); + public ChatModel Chat { get; } = new(); } diff --git a/GameServer/Program.cs b/GameServer/Program.cs index bae3068..945ec11 100644 --- a/GameServer/Program.cs +++ b/GameServer/Program.cs @@ -1,5 +1,6 @@ using Core.Config; using Core.Extensions; +using GameServer.Controllers.ChatCommands; using GameServer.Controllers.Factory; using GameServer.Controllers.Manager; using GameServer.Extensions; @@ -22,22 +23,37 @@ internal static class Program { private static async Task Main(string[] args) { + Console.Title = "Wuthering Waves | Game Server"; + Console.WriteLine(" __ __ __ .__ .__ __ __ \r\n/ \\ / \\__ ___/ |_| |__ ___________|__| ____ ____ / \\ / \\_____ ___ __ ____ ______\r\n\\ \\/\\/ / | \\ __\\ | \\_/ __ \\_ __ \\ |/ \\ / ___\\ \\ \\/\\/ /\\__ \\\\ \\/ // __ \\ / ___/\r\n \\ /| | /| | | Y \\ ___/| | \\/ | | \\/ /_/ > \\ / / __ \\\\ /\\ ___/ \\___ \\ \r\n \\__/\\ / |____/ |__| |___| /\\___ >__| |__|___| /\\___ / \\__/\\ / (____ /\\_/ \\___ >____ >\r\n \\/ \\/ \\/ \\//_____/ \\/ \\/ \\/ \\/ \n"); + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Logging.AddConsole(); builder.SetupConfiguration(); builder.Services.UseLocalResources() .AddControllers() + .AddCommands() .AddSingleton() .AddSingleton().AddScoped() .AddScoped().AddSingleton() .AddScoped().AddScoped() .AddSingleton() .AddScoped().AddScoped().AddScoped() - .AddScoped().AddScoped() + .AddScoped().AddScoped().AddScoped() .AddHostedService(); - await builder.Build().RunAsync(); + IHost host = builder.Build(); + + ILogger logger = host.Services.GetRequiredService().CreateLogger("WutheringWaves"); + logger.LogInformation("Support: discord.gg/reversedrooms or discord.xeondev.com"); + logger.LogInformation("Preparing server..."); + + host.Services.GetRequiredService().ApplicationStarted.Register(() => + { + logger.LogInformation("Server started! Let's play Wuthering Waves!"); + }); + + await host.RunAsync(); } private static void SetupConfiguration(this HostApplicationBuilder builder) diff --git a/GameServer/Systems/Entity/MonsterEntity.cs b/GameServer/Systems/Entity/MonsterEntity.cs index 2049bc0..d298df0 100644 --- a/GameServer/Systems/Entity/MonsterEntity.cs +++ b/GameServer/Systems/Entity/MonsterEntity.cs @@ -28,10 +28,19 @@ internal class MonsterEntity : EntityBase aiComponent.AiTeamInitId = 100; EntityFsmComponent fsm = ComponentSystem.Create(); + fsm.Fsms.Add(new DFsm { FsmId = 10007, // Main State Machine - CurrentState = 10009, // Standby Entry + CurrentState = 10013, // Battle Branching + Status = 1, // ?? + Flag = (int)EFsmStateFlag.Confirmed + }); + + fsm.Fsms.Add(new DFsm + { + FsmId = 10007, // Main State Machine + CurrentState = 10015, // Moving Combat Status = 1, // ?? Flag = (int)EFsmStateFlag.Confirmed }); diff --git a/GameServer/icon.ico b/GameServer/icon.ico new file mode 100644 index 0000000..2c6c83a Binary files /dev/null and b/GameServer/icon.ico differ diff --git a/SDKServer/SDKServer.csproj b/SDKServer/SDKServer.csproj index 38e7958..9174938 100644 --- a/SDKServer/SDKServer.csproj +++ b/SDKServer/SDKServer.csproj @@ -4,8 +4,13 @@ net8.0 enable enable + icon.ico + + + + diff --git a/SDKServer/icon.ico b/SDKServer/icon.ico new file mode 100644 index 0000000..2c6c83a Binary files /dev/null and b/SDKServer/icon.ico differ