Unlock all characters, implement team editing

This commit is contained in:
xeon 2024-02-12 01:23:13 +03:00
parent 6ee5273c67
commit ac6b50fa6a
22 changed files with 2925 additions and 14 deletions

View file

@ -0,0 +1,12 @@
namespace Core.Config.Attributes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class ConfigCollectionAttribute : Attribute
{
public string Path { get; }
public ConfigCollectionAttribute(string path)
{
Path = path;
}
}

View file

@ -0,0 +1,49 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace Core.Config;
public class ConfigCollection
{
private readonly ImmutableDictionary<int, IConfig> _configs;
public ConfigCollection(JsonDocument json, Type type)
{
_configs = LoadConfigs(json, type);
}
public int Count => _configs.Count;
public TConfig At<TConfig>(int index) where TConfig : IConfig
{
return (TConfig)_configs.Values.ElementAt(index);
}
public IEnumerable<TConfig> Enumerate<TConfig>() where TConfig : IConfig
{
return _configs.Values.Cast<TConfig>();
}
public bool TryGet<TConfig>(int identifier, [NotNullWhen(true)] out TConfig? config) where TConfig : IConfig
{
bool result = _configs.TryGetValue(identifier, out IConfig? cfg);
config = (TConfig?)cfg;
return result;
}
private static ImmutableDictionary<int, IConfig> LoadConfigs(JsonDocument json, Type type)
{
var builder = ImmutableDictionary.CreateBuilder<int, IConfig>();
foreach (JsonElement element in json.RootElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object) throw new InvalidDataException($"LoadConfigs: expected array of {JsonValueKind.Object}, got array of {element.ValueKind}");
IConfig configItem = (element.Deserialize(type) as IConfig)!;
builder.Add(configItem.Identifier, configItem);
}
return builder.ToImmutable();
}
}

View file

@ -0,0 +1,57 @@
using System.Collections.Immutable;
using System.Reflection;
using Core.Config.Attributes;
using Core.Resources;
using Microsoft.Extensions.Logging;
namespace Core.Config;
public class ConfigManager
{
private readonly ImmutableDictionary<ConfigType, ConfigCollection> _collectionsByEnum;
private readonly ImmutableDictionary<Type, ConfigCollection> _collectionsByType;
public ConfigManager(ILogger<ConfigManager> logger, IResourceProvider resourceProvider)
{
(_collectionsByEnum, _collectionsByType) = LoadConfigCollections(resourceProvider);
logger.LogInformation("Loaded {count} config collections", _collectionsByEnum.Count);
}
public ConfigCollection GetCollection<TConfigType>()
{
return _collectionsByType[typeof(TConfigType)];
}
public ConfigCollection GetCollection(ConfigType type)
{
return _collectionsByEnum[type];
}
public TConfig? GetConfig<TConfig>(int id) where TConfig : IConfig
{
if (_collectionsByType[typeof(TConfig)].TryGet(id, out TConfig? config))
return config;
return default;
}
private static (ImmutableDictionary<ConfigType, ConfigCollection>, ImmutableDictionary<Type, ConfigCollection>) LoadConfigCollections(IResourceProvider resourceProvider)
{
var builderByEnum = ImmutableDictionary.CreateBuilder<ConfigType, ConfigCollection>();
var builderByType = ImmutableDictionary.CreateBuilder<Type, ConfigCollection>();
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.IsAssignableTo(typeof(IConfig)) && !type.IsAbstract);
foreach (Type type in types)
{
ConfigCollectionAttribute? attribute = type.GetCustomAttribute<ConfigCollectionAttribute>();
if (attribute == null) continue;
ConfigCollection collection = new(resourceProvider.GetJsonResource("data/config/" + attribute.Path), type);
builderByEnum.Add(collection.At<IConfig>(0).Type, collection);
builderByType.Add(collection.At<IConfig>(0).GetType(), collection);
}
return (builderByEnum.ToImmutable(), builderByType.ToImmutable());
}
}

View file

@ -0,0 +1,5 @@
namespace Core.Config;
public enum ConfigType
{
RoleInfo
}

6
Core/Config/IConfig.cs Normal file
View file

@ -0,0 +1,6 @@
namespace Core.Config;
public interface IConfig
{
ConfigType Type { get; }
int Identifier { get; }
}

View file

@ -0,0 +1,69 @@
using Core.Config.Attributes;
namespace Core.Config;
[ConfigCollection("role/roleinfo.json")]
public class RoleInfoConfig : IConfig
{
public ConfigType Type => ConfigType.RoleInfo;
public int Identifier => Id;
public int Id { get; set; }
public int QualityId { get; set; }
public int RoleType { get; set; }
public bool IsTrial { get; set; }
public string Name { get; set; } = string.Empty;
public string NickName { get; set; } = string.Empty;
public string Introduction { get; set; } = string.Empty;
public int ParentId { get; set; }
public int Priority { get; set; }
public int PropertyId { get; set; }
public List<int> ShowProperty { get; set; } = [];
public int ElementId { get; set; }
public string RoleHeadIconLarge { get; set; } = string.Empty;
public string RoleHeadIconBig { get; set; } = string.Empty;
public string Card { get; set; } = string.Empty;
public string RoleHeadIcon { get; set; } = string.Empty;
public string FormationRoleCard { get; set; } = string.Empty;
public string RoleStand { get; set; } = string.Empty;
public int MeshId { get; set; }
public int UiMeshId { get; set; }
public string RoleBody { get; set; } = string.Empty;
public int BreachModel { get; set; }
public int SpecialEnergyBarId { get; set; }
public string CameraConfig { get; set; } = string.Empty;
public int CameraFloatHeight { get; set; }
public int EntityProperty { get; set; }
public int MaxLevel { get; set; }
public int LevelConsumeId { get; set; }
public int BreachId { get; set; }
public int SkillId { get; set; }
public int SkillTreeGroupId { get; set; }
public int ResonanceId { get; set; }
public int ResonantChainGroupId { get; set; }
public bool IsShow { get; set; }
public int InitWeaponItemId { get; set; }
public int WeaponType { get; set; }
public string SkillDAPath { get; set; } = string.Empty;
public string SkillLockDAPath { get; set; } = string.Empty;
public string UiScenePerformanceABP { get; set; } = string.Empty;
public int LockOnDefaultId { get; set; }
public int LockOnLookOnId { get; set; }
public string SkillEffectDA { get; set; } = string.Empty;
public string FootStepState { get; set; } = string.Empty;
public int PartyId { get; set; }
public string AttributesDescription { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public int ItemQualityId { get; set; }
public string ObtainedShowDescription { get; set; } = string.Empty;
public int NumLimit { get; set; }
public bool ShowInBag { get; set; }
public List<float> WeaponScale { get; set; } = [];
public bool Intervene { get; set; }
public string CharacterVoice { get; set; } = string.Empty;
public int TrialRole { get; set; }
public bool IsAim { get; set; }
public int RoleGuide { get; set; }
public int RedDotDisableRule { get; set; }
}

19
Core/Core.csproj Normal file
View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="data\config\*\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,11 @@
using Core.Resources;
using Microsoft.Extensions.DependencyInjection;
namespace Core.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection UseLocalResources(this IServiceCollection services)
{
return services.AddSingleton<IResourceProvider, LocalResourceProvider>();
}
}

View file

@ -0,0 +1,7 @@
using System.Text.Json;
namespace Core.Resources;
public interface IResourceProvider
{
JsonDocument GetJsonResource(string path);
}

View file

@ -0,0 +1,11 @@
using System.Text.Json;
namespace Core.Resources;
internal class LocalResourceProvider : IResourceProvider
{
public JsonDocument GetJsonResource(string path)
{
using FileStream fileStream = File.Open(path, FileMode.Open, FileAccess.Read);
return JsonDocument.Parse(fileStream);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
using GameServer.Controllers.Attributes;
using System.Security.Principal;
using GameServer.Controllers.Attributes;
using GameServer.Models;
using GameServer.Network;
using GameServer.Network.Messages;
@ -70,6 +71,69 @@ internal class CreatureController : Controller
return Response(MessageId.SceneLoadingFinishResponse, new SceneLoadingFinishResponse());
}
[GameEvent(GameEventType.FormationUpdated)]
public async Task OnFormationUpdated()
{
// Remove old entities
IEnumerable<PlayerEntity> oldEntities = _entitySystem.EnumerateEntities()
.Where(e => e is PlayerEntity entity && entity.PlayerId == _modelManager.Player.Id)
.Cast<PlayerEntity>().ToArray();
foreach (PlayerEntity oldEntity in oldEntities)
{
_entitySystem.Destroy(oldEntity);
}
await Session.Push(MessageId.EntityRemoveNotify, new EntityRemoveNotify
{
IsRemove = true,
RemoveInfos =
{
oldEntities.Select(entity => new EntityRemoveInfo
{
EntityId = entity.Id,
Type = (int)entity.Type
})
}
});
// Spawn new entities
CreateTeamPlayerEntities();
IEnumerable<PlayerEntity> newEntities = _entitySystem.EnumerateEntities()
.Where(e => e is PlayerEntity entity && entity.PlayerId == _modelManager.Player.Id)
.Cast<PlayerEntity>();
await Session.Push(MessageId.EntityAddNotify, new EntityAddNotify
{
IsAdd = true,
EntityPbs =
{
newEntities.Select(entity => entity.Pb)
}
});
_modelManager.Creature.PlayerEntityId = newEntities.First().Id;
await Session.Push(MessageId.UpdatePlayerAllFightRoleNotify, new UpdatePlayerAllFightRoleNotify
{
PlayerId = _modelManager.Player.Id,
FightRoleInfos =
{
newEntities.Select(entity => new FightRoleInformation
{
EntityId = entity.Id,
CurHp = 1000,
MaxHp = 1000,
IsControl = _modelManager.Creature.PlayerEntityId == entity.Id,
RoleId = entity.ConfigId,
RoleLevel = 1,
})
}
});
}
[GameEvent(GameEventType.VisionSkillChanged)]
public async Task OnVisionSkillChanged()
{
@ -86,7 +150,13 @@ internal class CreatureController : Controller
public PlayerEntity? GetPlayerEntity()
{
return _entitySystem.EnumerateEntities().FirstOrDefault(entity => entity.Id == _modelManager.Creature.PlayerEntityId) as PlayerEntity;
return _entitySystem.Get<PlayerEntity>(_modelManager.Creature.PlayerEntityId);
}
public PlayerEntity? GetPlayerEntityByRoleId(int roleId)
{
return _entitySystem.EnumerateEntities()
.FirstOrDefault(e => e is PlayerEntity playerEntity && playerEntity.ConfigId == roleId && playerEntity.PlayerId == _modelManager.Player.Id) as PlayerEntity;
}
public async Task SwitchPlayerEntity(int roleId)
@ -151,9 +221,9 @@ internal class CreatureController : Controller
private void CreateTeamPlayerEntities()
{
for (int i = 0; i < _modelManager.Player.Characters.Length; i++)
for (int i = 0; i < _modelManager.Formation.RoleIds.Length; i++)
{
PlayerEntity entity = _entityFactory.CreatePlayer(_modelManager.Player.Characters[i], _modelManager.Player.Id);
PlayerEntity entity = _entityFactory.CreatePlayer(_modelManager.Formation.RoleIds[i], _modelManager.Player.Id);
entity.Pos = new()
{
X = 4000,

View file

@ -2,6 +2,8 @@
using GameServer.Models;
using GameServer.Network;
using GameServer.Network.Messages;
using GameServer.Systems.Entity;
using GameServer.Systems.Event;
using Protocol;
namespace GameServer.Controllers;
@ -29,6 +31,18 @@ internal class FormationController : Controller
},
});
[NetEvent(MessageId.UpdateFormationRequest)]
public async Task<ResponseMessage> OnUpdateFormationRequest(UpdateFormationRequest request, EventSystem eventSystem)
{
_modelManager.Formation.Set([.. request.Formation.RoleIds]);
await eventSystem.Emit(GameEventType.FormationUpdated);
return Response(MessageId.UpdateFormationResponse, new UpdateFormationResponse
{
Formation = request.Formation
});
}
[NetEvent(MessageId.FormationAttrRequest)]
public ResponseMessage OnFormationAttrRequest() => Response(MessageId.FormationAttrResponse, new FormationAttrResponse());
}

View file

@ -1,4 +1,5 @@
using GameServer.Controllers.Attributes;
using Core.Config;
using GameServer.Controllers.Attributes;
using GameServer.Models;
using GameServer.Network;
using GameServer.Network.Messages;
@ -14,7 +15,7 @@ internal class RoleController : Controller
}
[GameEvent(GameEventType.EnterGame)]
public async Task OnEnterGame(ModelManager modelManager)
public async Task OnEnterGame(ModelManager modelManager, ConfigManager configManager)
{
PlayerModel player = modelManager.Player;
@ -22,9 +23,11 @@ internal class RoleController : Controller
{
RoleList =
{
player.Characters.Select(i => new roleInfo
configManager.GetCollection(ConfigType.RoleInfo)
.Enumerate<RoleInfoConfig>()
.Select(config => new roleInfo
{
RoleId = i,
RoleId = config.Id,
Level = 1
})
}

View file

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\KcpSharp\KcpSharp.csproj" />
<ProjectReference Include="..\Protocol\Protocol.csproj" />
</ItemGroup>

View file

@ -0,0 +1,18 @@
namespace GameServer.Models;
internal class FormationModel
{
public int[] RoleIds { get; }
public FormationModel()
{
RoleIds = new int[3];
}
public void Set(int[] roleIds)
{
for (int i = 0; i < RoleIds.Length; i++)
{
RoleIds[i] = roleIds[i];
}
}
}

View file

@ -21,9 +21,13 @@ internal class ModelManager
{
_playerModel = PlayerModel.CreateDefaultPlayer(_playerStartingValues.Value);
_creatureModel = new CreatureModel(_playerModel.Id);
Formation.Set(_playerStartingValues.Value.Characters);
}
public PlayerModel Player => _playerModel ?? throw new InvalidOperationException($"Trying to access {nameof(PlayerModel)} instance before initialization!");
public CreatureModel Creature => _creatureModel ?? throw new InvalidOperationException($"Trying to access {nameof(CreatureModel)} instance before initialization!");
public FormationModel Formation { get; } = new();
}

View file

@ -1,4 +1,6 @@
using GameServer.Controllers.Factory;
using Core.Config;
using Core.Extensions;
using GameServer.Controllers.Factory;
using GameServer.Controllers.Manager;
using GameServer.Extensions;
using GameServer.Models;
@ -24,7 +26,9 @@ internal static class Program
builder.Logging.AddConsole();
builder.SetupConfiguration();
builder.Services.AddControllers()
builder.Services.UseLocalResources()
.AddControllers()
.AddSingleton<ConfigManager>()
.AddSingleton<KcpGateway>().AddScoped<PlayerSession>()
.AddScoped<MessageManager>().AddSingleton<EventHandlerFactory>()
.AddScoped<RpcManager>().AddScoped<IRpcEndPoint, RpcSessionEndPoint>()

View file

@ -24,6 +24,11 @@ internal class EntitySystem
_entities.Add(entity);
}
public void Destroy(EntityBase entity)
{
_ = _entities.Remove(entity);
}
public void Activate(EntityBase entity)
{
entity.Activate();

View file

@ -5,5 +5,6 @@ internal enum GameEventType
EnterGame,
// Actions
FormationUpdated,
VisionSkillChanged
}

View file

@ -1,4 +1,5 @@
using GameServer.Controllers.Factory;
using Core.Config;
using GameServer.Controllers.Factory;
using GameServer.Network.Kcp;
using Microsoft.Extensions.Hosting;
@ -7,8 +8,9 @@ internal class WWGameServer : IHostedService
{
private readonly KcpGateway _gateway;
public WWGameServer(KcpGateway gateway, EventHandlerFactory messageHandlerFactory)
public WWGameServer(KcpGateway gateway, ConfigManager manager, EventHandlerFactory messageHandlerFactory)
{
_ = manager;
_ = messageHandlerFactory;
_gateway = gateway;
}

View file

@ -12,9 +12,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameServer", "GameServer\GameServer.csproj", "{78D639E8-D607-41F1-B0B8-AB1611ADE08F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KcpSharp", "KcpSharp\KcpSharp.csproj", "{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KcpSharp", "KcpSharp\KcpSharp.csproj", "{C2BDCF0A-C256-4E97-9D9A-45FF5C8614CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Protocol", "Protocol\Protocol.csproj", "{9900A88C-7818-4335-84F7-1538ECC8B338}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Protocol", "Protocol\Protocol.csproj", "{9900A88C-7818-4335-84F7-1538ECC8B338}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{C025BDED-6DC7-493D-8D10-05DCCB3072F3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -38,6 +40,10 @@ Global
{9900A88C-7818-4335-84F7-1538ECC8B338}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9900A88C-7818-4335-84F7-1538ECC8B338}.Release|Any CPU.Build.0 = Release|Any CPU
{C025BDED-6DC7-493D-8D10-05DCCB3072F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C025BDED-6DC7-493D-8D10-05DCCB3072F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C025BDED-6DC7-493D-8D10-05DCCB3072F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C025BDED-6DC7-493D-8D10-05DCCB3072F3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE