diff --git a/src/Prima.Core.Server/Data/Internal/TimerDataObject.cs b/src/Prima.Core.Server/Data/Internal/TimerDataObject.cs new file mode 100644 index 0000000..da4852a --- /dev/null +++ b/src/Prima.Core.Server/Data/Internal/TimerDataObject.cs @@ -0,0 +1,78 @@ +namespace Prima.Core.Server.Data.Internal; + +public class TimerDataObject : IDisposable +{ + private readonly object _lock = new object(); + public string Name { get; set; } + + public string Id { get; set; } + + public double IntervalInMs { get; set; } + + public Action Callback { get; set; } + + public bool Repeat { get; set; } + + public double RemainingTimeInMs = 0; + + public double DelayInMs { get; set; } + + + public void DecrementRemainingTime(double deltaTime) + { + if (Monitor.TryEnter(_lock)) + { + try + { + if (DelayInMs > 0) + { + DelayInMs -= deltaTime; + if (DelayInMs > 0) + { + return; + } + } + + RemainingTimeInMs -= deltaTime; + } + finally + { + Monitor.Exit(_lock); + } + } + } + + public void ResetRemainingTime() + { + if (Monitor.TryEnter(_lock)) + { + try + { + RemainingTimeInMs = IntervalInMs; + } + finally + { + Monitor.Exit(_lock); + } + } + } + + + public override string ToString() + { + return $"Timer: {Name}, Id: {Id}, Interval: {IntervalInMs}, RemainingTime: {RemainingTimeInMs}, Repeat: {Repeat}"; + } + + public void Dispose() + { + Callback = null; + Name = null; + Id = null; + IntervalInMs = 0; + RemainingTimeInMs = 0; + Repeat = false; + DelayInMs = 0; + + GC.SuppressFinalize(this); + } +} diff --git a/src/Prima.Core.Server/Interfaces/Services/ITimerService.cs b/src/Prima.Core.Server/Interfaces/Services/ITimerService.cs new file mode 100644 index 0000000..8b9b33d --- /dev/null +++ b/src/Prima.Core.Server/Interfaces/Services/ITimerService.cs @@ -0,0 +1,12 @@ +using Orion.Core.Server.Interfaces.Services.Base; + +namespace Prima.Core.Server.Interfaces.Services; + +public interface ITimerService : IOrionService, IOrionStartService, IDisposable +{ + string RegisterTimer(string name, double intervalInMs, Action callback, double delayInMs = 0, bool repeat = false); + + void UnregisterTimer(string timerId); + + void UnregisterAllTimers(); +} diff --git a/src/Prima.Core.Server/Modules/Scripts/FileScriptModule.cs b/src/Prima.Core.Server/Modules/Scripts/FileScriptModule.cs new file mode 100644 index 0000000..1a1dfa4 --- /dev/null +++ b/src/Prima.Core.Server/Modules/Scripts/FileScriptModule.cs @@ -0,0 +1,61 @@ +using Orion.Core.Server.Attributes.Scripts; +using Orion.Core.Server.Data.Directories; +using Orion.Core.Server.Interfaces.Services.System; + +namespace Prima.Core.Server.Modules.Scripts; + +[ScriptModule("files")] +public class FileScriptModule +{ + private readonly DirectoriesConfig _directoriesConfig; + + private readonly IScriptEngineService _scriptEngineService; + + public FileScriptModule(DirectoriesConfig directoriesConfig, IScriptEngineService scriptEngineService) + { + _directoriesConfig = directoriesConfig; + _scriptEngineService = scriptEngineService; + } + + + [ScriptFunction("Include a script file")] + public void IncludeScript(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentNullException(nameof(fileName), "File name cannot be null or empty"); + } + + var filePath = Path.Combine(_directoriesConfig.Root, fileName); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Script file '{fileName}' not found in the scripts directory."); + } + + _scriptEngineService.ExecuteScriptFile(filePath); + } + + [ScriptFunction("Include all script files in a directory")] + public void IncludeScripts(string directory) + { + if (string.IsNullOrEmpty(directory)) + { + throw new ArgumentNullException(nameof(directory), "Directory name cannot be null or empty"); + } + + var directoryPath = Path.Combine(_directoriesConfig.Root, directory); + + if (!Directory.Exists(directoryPath)) + { + throw new DirectoryNotFoundException($"Directory '{directory}' not found in the scripts directory."); + } + + var scriptFiles = Directory.GetFiles(directoryPath, "*.js"); + + foreach (var scriptFile in scriptFiles) + { + _scriptEngineService.ExecuteScriptFile(scriptFile); + } + } +} diff --git a/src/Prima.Core.Server/Prima.Core.Server.csproj b/src/Prima.Core.Server/Prima.Core.Server.csproj index 1e4df9c..f64a49a 100644 --- a/src/Prima.Core.Server/Prima.Core.Server.csproj +++ b/src/Prima.Core.Server/Prima.Core.Server.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Prima.Network/Packets/ClientVersionReq.cs b/src/Prima.Network/Packets/ClientVersionReq.cs new file mode 100644 index 0000000..2558876 --- /dev/null +++ b/src/Prima.Network/Packets/ClientVersionReq.cs @@ -0,0 +1,15 @@ +using Prima.Network.Packets.Base; + +namespace Prima.Network.Packets; + +public class ClientVersionReq : BaseUoNetworkPacket +{ + public ClientVersionReq() : base(0xBD, 3) + { + } + + public override Span Write() + { + return new byte[] { 0x00, 0x03 }; + } +} diff --git a/src/Prima.Network/Prima.Network.csproj b/src/Prima.Network/Prima.Network.csproj index 1ca1052..3b67775 100644 --- a/src/Prima.Network/Prima.Network.csproj +++ b/src/Prima.Network/Prima.Network.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/src/Prima.Server/Handlers/CharacterCreationHandler.cs b/src/Prima.Server/Handlers/CharacterCreationHandler.cs index aaff911..4537955 100644 --- a/src/Prima.Server/Handlers/CharacterCreationHandler.cs +++ b/src/Prima.Server/Handlers/CharacterCreationHandler.cs @@ -1,15 +1,29 @@ +using Orion.Core.Server.Interfaces.Services.System; using Prima.Core.Server.Data.Session; using Prima.Core.Server.Handlers.Base; using Prima.Core.Server.Interfaces.Listeners; using Prima.Core.Server.Interfaces.Services; +using Prima.Network.Packets; +using Prima.Server.Modules.Scripts; +using Prima.UOData.Data.EventData; +using Prima.UOData.Interfaces.Services; using Prima.UOData.Packets; namespace Prima.Server.Handlers; public class CharacterCreationHandler : BasePacketListenerHandler, INetworkPacketListener { - public CharacterCreationHandler(ILogger logger, INetworkService networkService, IServiceProvider serviceProvider) : base(logger, networkService, serviceProvider) + private readonly IScriptEngineService _scriptEngineService; + + private readonly IMapService _mapService; + + public CharacterCreationHandler( + ILogger logger, INetworkService networkService, IServiceProvider serviceProvider, + IScriptEngineService scriptEngineService, IMapService mapService + ) : base(logger, networkService, serviceProvider) { + _scriptEngineService = scriptEngineService; + _mapService = mapService; } protected override void RegisterHandlers() @@ -19,6 +33,34 @@ protected override void RegisterHandlers() public async Task OnPacketReceived(NetworkSession session, CharacterCreation packet) { + // TODO: persist character creation data + + TriggerCharacterCreatedEvent(packet); + + await session.SendPacketAsync(new ClientVersionReq()); + } + + private void TriggerCharacterCreatedEvent(CharacterCreation packet) + { + var eventArgs = new CharacterCreatedEventArgs( + packet.Name, + packet.IsFemale, + packet.Hue, + packet.Int, + packet.Str, + packet.Dex, + _mapService.GetAvailableStartingCities()[packet.StartingLocation], + packet.Skills, + packet.ShirtColor, + packet.PantsColor, + packet.HairStyle, + packet.HairColor, + packet.FacialHair, + packet.FacialHairColor, + packet.Profession, + packet.Race + ); + _scriptEngineService.ExecuteCallback(nameof(EventScriptModule.OnCharacterCreated), eventArgs); } } diff --git a/src/Prima.Server/Modules/Scripts/EventScriptModule.cs b/src/Prima.Server/Modules/Scripts/EventScriptModule.cs index b6a1957..d2447fe 100644 --- a/src/Prima.Server/Modules/Scripts/EventScriptModule.cs +++ b/src/Prima.Server/Modules/Scripts/EventScriptModule.cs @@ -1,6 +1,7 @@ using Orion.Core.Server.Attributes.Scripts; using Orion.Core.Server.Interfaces.Services.System; using Prima.Core.Server.Contexts; +using Prima.UOData.Data.EventData; namespace Prima.Server.Modules.Scripts; @@ -50,4 +51,25 @@ public void OnUserLogin(Action action) } ); } + + [ScriptFunction("Register a callback when user creates a character")] + public void OnCharacterCreated(Action action) + { + _scriptEngineService.AddCallback( + nameof(OnCharacterCreated), + context => + { + if (context == null) + { + throw new ArgumentNullException(nameof(context), "Context cannot be null"); + return; + } + + if (context[0] is CharacterCreatedEventArgs characterCreatedEventArgs) + { + action(characterCreatedEventArgs); + } + } + ); + } } diff --git a/src/Prima.Server/Modules/Scripts/TimerScriptModule.cs b/src/Prima.Server/Modules/Scripts/TimerScriptModule.cs new file mode 100644 index 0000000..b6b14fe --- /dev/null +++ b/src/Prima.Server/Modules/Scripts/TimerScriptModule.cs @@ -0,0 +1,69 @@ +using Orion.Core.Server.Attributes.Scripts; +using Prima.Core.Server.Interfaces.Services; + +namespace Prima.Server.Modules.Scripts; + +[ScriptModule("timers")] +public class TimerScriptModule +{ + private readonly ITimerService _timerService; + + public TimerScriptModule(ITimerService timerService) + { + _timerService = timerService; + } + + + [ScriptFunction("Register a timer")] + public string Register( + string name, int intervalInSeconds, Action callback, int delayInSeconds = 0, bool isRepeat = false + ) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name), "Timer name cannot be null or empty"); + } + + if (intervalInSeconds <= 0) + { + throw new ArgumentOutOfRangeException(nameof(intervalInSeconds), "Interval must be greater than zero"); + } + + if (callback == null) + { + throw new ArgumentNullException(nameof(callback), "Callback cannot be null"); + } + + return _timerService.RegisterTimer( + name, + TimeSpan.FromSeconds(intervalInSeconds).TotalMilliseconds, + callback, + TimeSpan.FromSeconds(delayInSeconds).TotalMilliseconds, + isRepeat + ); + } + + [ScriptFunction("Register a timer that repeats")] + public string Repeated(string name, int intervalInSeconds, Action callback, int delayInSeconds = 0) + { + return Register(name, intervalInSeconds, callback, delayInSeconds, true); + } + + + [ScriptFunction("Register a timer that runs once")] + public string OneShot(string name, int intervalInSeconds, Action callback, int delayInSeconds = 0) + { + return Register(name, intervalInSeconds, callback, delayInSeconds, false); + } + + [ScriptFunction("Unregister a timer")] + public void Unregister(string timerId) + { + if (string.IsNullOrEmpty(timerId)) + { + throw new ArgumentNullException(nameof(timerId), "Timer ID cannot be null or empty"); + } + + _timerService.UnregisterTimer(timerId); + } +} diff --git a/src/Prima.Server/Program.cs b/src/Prima.Server/Program.cs index a633731..f3d7394 100644 --- a/src/Prima.Server/Program.cs +++ b/src/Prima.Server/Program.cs @@ -17,6 +17,7 @@ using Prima.Core.Server.Data.Options; using Prima.Core.Server.Interfaces.Services; using Prima.Core.Server.Modules.Container; +using Prima.Core.Server.Modules.Scripts; using Prima.Core.Server.Types; using Prima.Network.Modules; using Prima.Server.Handlers; @@ -103,6 +104,7 @@ static async Task Main(string[] args) .AddService() .AddService() .AddService() + .AddService() .AddService() ; @@ -110,6 +112,8 @@ static async Task Main(string[] args) .AddScriptModule() .AddScriptModule() .AddScriptModule() + .AddScriptModule() + .AddScriptModule() .AddScriptModule(); builder.Services diff --git a/src/Prima.Server/Services/NetworkService.cs b/src/Prima.Server/Services/NetworkService.cs index a2dda0c..759e372 100644 --- a/src/Prima.Server/Services/NetworkService.cs +++ b/src/Prima.Server/Services/NetworkService.cs @@ -324,7 +324,7 @@ private async Task HandleIncomingMessages(NetworkMessageData data) public async Task StartAsync(CancellationToken cancellationToken = default) { var loginServers = - GetListeningAddresses(IPEndPoint.Parse(IPAddress.Any.ToString() + _serverConfig.TcpServer.LoginPort)).ToList(); + GetListeningAddresses(IPEndPoint.Parse("0.0.0.0:" + _serverConfig.TcpServer.LoginPort)).ToList(); foreach (var loginServer in loginServers) { @@ -380,7 +380,11 @@ public async Task StopAsync(CancellationToken cancellationToken = default) public void RegisterPacketListener(INetworkPacketListener listener) where TPacket : IUoNetworkPacket, new() { var packet = new TPacket(); - _logger.LogInformation("Registering packet listener for {PacketType}", "0x" + packet.OpCode.ToString("X2")); + _logger.LogInformation( + "Registering packet listener for {PacketType} (PacketType: {Type})", + "0x" + packet.OpCode.ToString("X2"), + packet.GetType().Name + ); if (!_listeners.TryGetValue(packet.OpCode, out var packetListeners)) { diff --git a/src/Prima.Server/Services/TimerService.cs b/src/Prima.Server/Services/TimerService.cs new file mode 100644 index 0000000..7b79b97 --- /dev/null +++ b/src/Prima.Server/Services/TimerService.cs @@ -0,0 +1,148 @@ +using System.Collections.Concurrent; +using Orion.Foundations.Pool; +using Prima.Core.Server.Data.Internal; +using Prima.Core.Server.Interfaces.Services; + +namespace Prima.Server.Services; + +public class TimerService : ITimerService +{ + private readonly ILogger _logger; + private readonly IEventLoopService _eventLoopService; + + private readonly ObjectPool _timerDataPool = new(5); + + private readonly SemaphoreSlim _timerSemaphore = new(1, 1); + private readonly BlockingCollection _timers = new(); + + public TimerService(ILogger logger, IEventLoopService eventLoopService) + { + _logger = logger; + _eventLoopService = eventLoopService; + } + + private void EventLoopServiceOnOnTick(double tickDurationMs) + { + _timerSemaphore.Wait(); + + foreach (var timer in _timers) + { + timer.DecrementRemainingTime(tickDurationMs); + + if (timer.RemainingTimeInMs <= 0) + { + try + { + timer.Callback?.Invoke(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing timer callback for {TimerId}", timer.Id); + } + + if (timer.Repeat) + { + timer.ResetRemainingTime(); + } + else + { + _timers.TryTake(out var _); + _logger.LogInformation("Unregistering timer: {TimerId}", timer.Id); + } + } + } + + _timerSemaphore.Release(); + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + _eventLoopService.OnTick += EventLoopServiceOnOnTick; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public string RegisterTimer(string name, double intervalInMs, Action callback, double delayInMs = 0, bool repeat = false) + { + var existingTimer = _timers.FirstOrDefault(t => t.Name == name); + + if (existingTimer != null) + { + _logger.LogWarning("Timer with name {Name} already exists. Unregistering it.", name); + UnregisterTimer(existingTimer.Id); + } + + _timerSemaphore.Wait(); + + var timerId = Guid.NewGuid().ToString(); + var timer = _timerDataPool.Get(); + + timer.Name = name; + timer.Id = timerId; + timer.IntervalInMs = intervalInMs; + timer.Callback = callback; + timer.Repeat = repeat; + timer.RemainingTimeInMs = intervalInMs; + timer.DelayInMs = delayInMs; + + + _timers.Add(timer); + + _timerSemaphore.Release(); + + _logger.LogDebug( + "Registering timer: {TimerId}, Interval: {IntervalInSeconds} ms, Repeat: {Repeat}", + timerId, + intervalInMs, + repeat + ); + + return timerId; + } + + public void UnregisterTimer(string timerId) + { + _timerSemaphore.Wait(); + + var timer = _timers.FirstOrDefault(t => t.Id == timerId); + + if (timer != null) + { + _timers.TryTake(out timer); + _logger.LogInformation("Unregistering timer: {TimerId}", timer.Id); + _timerDataPool.Return(timer); + } + else + { + _logger.LogWarning("Timer with ID {TimerId} not found", timerId); + } + + _timerSemaphore.Release(); + } + + public void UnregisterAllTimers() + { + _timerSemaphore.Wait(); + + while (_timers.TryTake(out var timer)) + { + _logger.LogInformation("Unregistering timer: {TimerId}", timer.Id); + } + + _timerSemaphore.Release(); + } + + public void Dispose() + { + _timerSemaphore.Dispose(); + _timers.Dispose(); + + _eventLoopService.OnTick -= EventLoopServiceOnOnTick; + + GC.SuppressFinalize(this); + } +} diff --git a/src/Prima.UOData/Context/UOContext.cs b/src/Prima.UOData/Context/UOContext.cs index 185bf0c..8092946 100644 --- a/src/Prima.UOData/Context/UOContext.cs +++ b/src/Prima.UOData/Context/UOContext.cs @@ -1,12 +1,13 @@ +using System.Runtime.CompilerServices; using Prima.Core.Server.Data.Uo; using Prima.Core.Server.Types.Uo; using Prima.UOData.Data; +using Prima.UOData.Types; namespace Prima.UOData.Context; public static class UOContext { - public static int SlotLimit { get; set; } = 5; public static ClientVersion ClientVersion { get; set; } @@ -14,4 +15,26 @@ public static class UOContext public static Expansion Expansion { get; set; } public static ExpansionInfo ExpansionInfo { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasProtocolChanges(ProtocolChanges changes) => (ClientVersion.ProtocolChanges & changes) != 0; + + public static bool NewSpellbook => HasProtocolChanges(ProtocolChanges.NewSpellbook); + public static bool DamagePacket => HasProtocolChanges(ProtocolChanges.DamagePacket); + public static bool BuffIcon => HasProtocolChanges(ProtocolChanges.BuffIcon); + public static bool NewHaven => HasProtocolChanges(ProtocolChanges.NewHaven); + public static bool ContainerGridLines => HasProtocolChanges(ProtocolChanges.ContainerGridLines); + public static bool ExtendedSupportedFeatures => HasProtocolChanges(ProtocolChanges.ExtendedSupportedFeatures); + public static bool StygianAbyss => HasProtocolChanges(ProtocolChanges.StygianAbyss); + public static bool HighSeas => HasProtocolChanges(ProtocolChanges.HighSeas); + public static bool NewCharacterList => HasProtocolChanges(ProtocolChanges.NewCharacterList); + public static bool NewCharacterCreation => HasProtocolChanges(ProtocolChanges.NewCharacterCreation); + public static bool ExtendedStatus => HasProtocolChanges(ProtocolChanges.ExtendedStatus); + public static bool NewMobileIncoming => HasProtocolChanges(ProtocolChanges.NewMobileIncoming); + public static bool NewSecureTrading => HasProtocolChanges(ProtocolChanges.NewSecureTrading); + + //public static bool IsUOTDClient => HasFlag(ClientFlags.UOTD) || ClientVersion?.Type == ClientType.UOTD; + public static bool IsKRClient => ClientVersion?.Type == ClientType.KR; + public static bool IsSAClient => ClientVersion?.Type == ClientType.SA; + public static bool IsEnhancedClient => ClientVersion?.Type is ClientType.KR or ClientType.SA; } diff --git a/src/Prima.UOData/Data/EventData/CharacterCreatedEventArgs.cs b/src/Prima.UOData/Data/EventData/CharacterCreatedEventArgs.cs new file mode 100644 index 0000000..cc1e15d --- /dev/null +++ b/src/Prima.UOData/Data/EventData/CharacterCreatedEventArgs.cs @@ -0,0 +1,61 @@ +using Prima.UOData.Data.Map; +using Prima.UOData.Types; + +namespace Prima.UOData.Data.EventData; + +public class CharacterCreatedEventArgs( + // NetState state, IAccount a, + string name, + bool female, + int hue, + int inte, + int str, + int dex, + CityInfo city, + Dictionary skills, + int shirtHue, + int pantsHue, + int hairId, + int hairHue, + int beardId, + int beardHue, + ProfessionInfo profession, + Race race +) +{ + // public NetState State { get; } = state; + // + // public IAccount Account { get; } = a; + // + // public Mobile Mobile { get; set; } + + public string Name { get; } = name; + + public bool Female { get; } = female; + + public int Hue { get; } = hue; + + public int Inte { get; } = inte; + public int Str { get; } = str; + public int Dex { get; } = dex; + + public CityInfo City { get; } = city; + + public Dictionary Skills { get; } = skills; + + public int ShirtHue { get; } = shirtHue; + + public int PantsHue { get; } = pantsHue; + + public int HairID { get; } = hairId; + + public int HairHue { get; } = hairHue; + + public int BeardID { get; } = beardId; + + public int BeardHue { get; } = beardHue; + + public ProfessionInfo Profession { get; set; } = profession; + + public Race Race { get; } = race; +} diff --git a/src/Prima.UOData/Data/Race.cs b/src/Prima.UOData/Data/Race.cs new file mode 100644 index 0000000..2bdace6 --- /dev/null +++ b/src/Prima.UOData/Data/Race.cs @@ -0,0 +1,158 @@ +using System.Runtime.CompilerServices; +using Orion.Foundations.Extensions; +using Prima.Core.Server.Types.Uo; + +namespace Prima.UOData.Data; + +public abstract class Race : ISpanParsable +{ + protected Race( + int raceID, int raceIndex, string name, string pluralName, int maleBody, int femaleBody, + int maleGhostBody, int femaleGhostBody, Expansion requiredExpansion + ) + { + RaceID = raceID; + RaceIndex = raceIndex; + RaceFlag = 1 << raceIndex; + + Name = name; + + MaleBody = maleBody; + FemaleBody = femaleBody; + MaleGhostBody = maleGhostBody; + FemaleGhostBody = femaleGhostBody; + + RequiredExpansion = requiredExpansion; + PluralName = pluralName; + } + + public static Race[] Races { get; } = new Race[0x100]; + + public static Race DefaultRace => Races[0]; + public static Race Human => Races[0]; + public static Race Elf => Races[1]; + public static Race Gargoyle => Races[2]; + + public static List AllRaces { get; } = new(); + + public const int AllowAllRaces = 0x7; // Race.Human.RaceFlag | Race.Elf.RaceFlag | Race.Gargoyle.RaceFlag + public const int AllowHumanOrElves = 0x3; // Race.Human.RaceFlag | Race.Elf.RaceFlag + public const int AllowElvesOnly = 0x2; // Race.Elf.RaceFlag + public const int AllowGargoylesOnly = 0x4; // Race.Gargoyle.RaceFlag + + public Expansion RequiredExpansion { get; } + + public int MaleBody { get; } + + public int MaleGhostBody { get; } + + public int FemaleBody { get; } + + public int FemaleGhostBody { get; } + + public int RaceID { get; } + + public int RaceIndex { get; } + + public int RaceFlag { get; } + + public string Name { get; set; } + + public string PluralName { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAllowedRace(Race race, int allowedRaceFlags) => (allowedRaceFlags & race.RaceFlag) != 0; + + public override string ToString() => Name; + + // public virtual bool ValidateHair(Mobile m, int itemID) => ValidateHair(m.Female, itemID); + + public abstract bool ValidateHair(bool female, int itemID); + + //public virtual int RandomHair(Mobile m) => RandomHair(m.Female); + + public abstract int RandomHair(bool female); + + //public virtual bool ValidateFacialHair(Mobile m, int itemID) => ValidateFacialHair(m.Female, itemID); + + public abstract bool ValidateFacialHair(bool female, int itemID); + + // public virtual int RandomFacialHair(Mobile m) => RandomFacialHair(m.Female); + + public abstract int RandomFacialHair(bool female); // For the *ahem* bearded ladies + + public abstract int ClipSkinHue(int hue); + public abstract int RandomSkinHue(); + + public abstract int ClipHairHue(int hue); + public abstract int RandomHairHue(); + + // public virtual int Body(Mobile m) => m.Alive ? AliveBody(m.Female) : GhostBody(m.Female); + + // public virtual int AliveBody(Mobile m) => AliveBody(m.Female); + + public virtual int AliveBody(bool female) => female ? FemaleBody : MaleBody; + + // public virtual int GhostBody(Mobile m) => GhostBody(m.Female); + + public virtual int GhostBody(bool female) => female ? FemaleGhostBody : MaleGhostBody; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Race Parse(string s) => Parse(s, null); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Race Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string s, IFormatProvider provider, out Race result) => + TryParse(s.AsSpan(), provider, out result); + + public static Race Parse(ReadOnlySpan s, IFormatProvider provider) + { + if (int.TryParse(s, out var index) && index >= 0 && index < Races.Length) + { + var race = Races[index]; + if (race != null) + { + return race; + } + } + + s = s.Trim(); + + for (var i = 0; i < Races.Length; ++i) + { + var race = Races[i]; + if (s.InsensitiveEquals(race.Name) || s.InsensitiveEquals(race.PluralName)) + { + return race; + } + } + + throw new FormatException($"The input string '{s}' was not in a correct format."); + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Race result) + { + if (int.TryParse(s, out var index) && index >= 0 && index < Races.Length) + { + result = Races[index]; + return result != null; + } + + s = s.Trim(); + + for (var i = 0; i < Races.Length; ++i) + { + var race = Races[i]; + if (s.InsensitiveEquals(race.Name) || s.InsensitiveEquals(race.PluralName)) + { + result = race; + return true; + } + } + + result = default; + return false; + } +} diff --git a/src/Prima.UOData/Data/RaceDefinitions.cs b/src/Prima.UOData/Data/RaceDefinitions.cs new file mode 100644 index 0000000..e4fec3f --- /dev/null +++ b/src/Prima.UOData/Data/RaceDefinitions.cs @@ -0,0 +1,318 @@ +using Orion.Foundations.Extensions; +using Orion.Foundations.Utils; +using Prima.Core.Server.Types.Uo; +using Prima.UOData.Utils; + +namespace Prima.UOData.Data; + +public static class RaceDefinitions +{ + public static void Configure() + { + /* Here we configure all races. Some notes: + * + * 1) The first 32 races are reserved for core use. + * 2) Race 0x7F is reserved for core use. + * 3) Race 0xFF is reserved for core use. + * 4) Changing or removing any predefined races may cause server instability. + */ + + RegisterRace(new Human(0, 0)); + RegisterRace(new Elf(1, 1)); + RegisterRace(new Gargoyle(2, 2)); + } + + public static void RegisterRace(Race race) + { + Race.Races[race.RaceIndex] = race; + Race.AllRaces.Add(race); + } + + private class Human : Race + { + public Human(int raceID, int raceIndex) + : base(raceID, raceIndex, "Human", "Humans", 400, 401, 402, 403, Expansion.None) + { + } + + public override bool ValidateHair(bool female, int itemID) + { + if (itemID == 0) + { + return true; + } + + if (female && itemID == 0x2048 || !female && itemID == 0x2046) + { + return false; // Buns & Receding Hair + } + + if (itemID is >= 0x203B and <= 0x203D) + { + return true; + } + + if (itemID is >= 0x2044 and <= 0x204A) + { + return true; + } + + return false; + } + + public override int RandomHair(bool female) // Random hair doesn't include baldness + { + return RandomUtils.Random(9) switch + { + 0 => 0x203B, // Short + 1 => 0x203C, // Long + 2 => 0x203D, // Pony Tail + 3 => 0x2044, // Mohawk + 4 => 0x2045, // Pageboy + 5 => 0x2047, // Afro + 6 => 0x2049, // Pig tails + 7 => 0x204A, // Krisna + _ => female ? 0x2046 : 0x2048 + }; + } + + public override bool ValidateFacialHair(bool female, int itemID) + { + if (itemID == 0) + { + return true; + } + + if (female) + { + return false; + } + + if (itemID is >= 0x203E and <= 0x2041) + { + return true; + } + + if (itemID is >= 0x204B and <= 0x204D) + { + return true; + } + + return false; + } + + public override int RandomFacialHair(bool female) + { + if (female) + { + return 0; + } + + var rand = RandomUtils.Random(7); + + return (rand < 4 ? 0x203E : 0x2047) + rand; + } + + public override int ClipSkinHue(int hue) + { + return hue switch + { + < 1002 => 1002, + > 1058 => 1058, + _ => hue + }; + } + + public override int RandomSkinHue() => RandomUtils.Random(1002, 57) | 0x8000; + + public override int ClipHairHue(int hue) + { + return hue switch + { + < 1102 => 1102, + > 1149 => 1149, + _ => hue + }; + } + + public override int RandomHairHue() => RandomUtils.Random(1102, 48); + } + + private class Elf : Race + { + private static readonly int[] m_SkinHues = + { + 0x0BF, 0x24D, 0x24E, 0x24F, 0x353, 0x361, 0x367, 0x374, + 0x375, 0x376, 0x381, 0x382, 0x383, 0x384, 0x385, 0x389, + 0x3DE, 0x3E5, 0x3E6, 0x3E8, 0x3E9, 0x430, 0x4A7, 0x4DE, + 0x51D, 0x53F, 0x579, 0x76B, 0x76C, 0x76D, 0x835, 0x903 + }; + + private static readonly int[] m_HairHues = + { + 0x034, 0x035, 0x036, 0x037, 0x038, 0x039, 0x058, 0x08E, + 0x08F, 0x090, 0x091, 0x092, 0x101, 0x159, 0x15A, 0x15B, + 0x15C, 0x15D, 0x15E, 0x128, 0x12F, 0x1BD, 0x1E4, 0x1F3, + 0x207, 0x211, 0x239, 0x251, 0x26C, 0x2C3, 0x2C9, 0x31D, + 0x31E, 0x31F, 0x320, 0x321, 0x322, 0x323, 0x324, 0x325, + 0x326, 0x369, 0x386, 0x387, 0x388, 0x389, 0x38A, 0x59D, + 0x6B8, 0x725, 0x853 + }; + + public Elf(int raceID, int raceIndex) + : base(raceID, raceIndex, "Elf", "Elves", 605, 606, 607, 608, Expansion.ML) + { + } + + public override bool ValidateHair(bool female, int itemID) + { + if (itemID == 0) + { + return true; + } + + if (female && itemID is 0x2FCD or 0x2FBF || !female && itemID is 0x2FCC or 0x2FD0) + { + return false; + } + + if (itemID is >= 0x2FBF and <= 0x2FC2) + { + return true; + } + + if (itemID is >= 0x2FCC and <= 0x2FD1) + { + return true; + } + + return false; + } + + public override int RandomHair(bool female) // Random hair doesn't include baldness + { + return RandomUtils.Random(8) switch + { + 0 => 0x2FC0, // Long Feather + 1 => 0x2FC1, // Short + 2 => 0x2FC2, // Mullet + 3 => 0x2FCE, // Knob + 4 => 0x2FCF, // Braided + 5 => 0x2FD1, // Spiked + 6 => female ? 0x2FCC : 0x2FBF, // Flower or Mid-long + _ => female ? 0x2FD0 : 0x2FCD + }; + } + + public override bool ValidateFacialHair(bool female, int itemID) => itemID == 0; + + public override int RandomFacialHair(bool female) => 0; + + public override int ClipSkinHue(int hue) + { + for (var i = 0; i < m_SkinHues.Length; i++) + { + if (m_SkinHues[i] == hue) + { + return hue; + } + } + + return m_SkinHues[0]; + } + + public override int RandomSkinHue() => m_SkinHues.RandomElement() | 0x8000; + + public override int ClipHairHue(int hue) + { + for (var i = 0; i < m_HairHues.Length; i++) + { + if (m_HairHues[i] == hue) + { + return hue; + } + } + + return m_HairHues[0]; + } + + public override int RandomHairHue() => m_HairHues.RandomElement(); + } + + private class Gargoyle : Race + { + private static readonly int[] m_HornHues = + { + 0x709, 0x70B, 0x70D, 0x70F, 0x711, 0x763, + 0x765, 0x768, 0x76B, 0x6F3, 0x6F1, 0x6EF, + 0x6E4, 0x6E2, 0x6E0, 0x709, 0x70B, 0x70D + }; + + public Gargoyle(int raceID, int raceIndex) + : base(raceID, raceIndex, "Gargoyle", "Gargoyles", 666, 667, 402, 403, Expansion.SA) + { + } + + public override bool ValidateHair(bool female, int itemID) + { + if (female == false) + { + return itemID is >= 0x4258 and <= 0x425F; + } + + return itemID is 0x4261 or 0x4262 or >= 0x4273 and <= 0x4275 or 0x42B0 or 0x42B1 or 0x42AA or 0x42AB; + } + + public override int RandomHair(bool female) + { + if (RandomUtils.Random(9) == 0) + { + return 0; + } + + if (!female) + { + return 0x4258 + RandomUtils.Random(8); + } + + return RandomUtils.Random(9) switch + { + 0 => 0x4261, + 1 => 0x4262, + 2 => 0x4273, + 3 => 0x4274, + 4 => 0x4275, + 5 => 0x42B0, + 6 => 0x42B1, + 7 => 0x42AA, + 8 => 0x42AB, + _ => 0 + }; + } + + public override bool ValidateFacialHair(bool female, int itemID) => + !female && itemID is >= 0x42AD and <= 0x42B0; + + public override int RandomFacialHair(bool female) => + female ? 0 : Utility.RandomList(0, 0x42AD, 0x42AE, 0x42AF, 0x42B0); + + public override int ClipSkinHue(int hue) => hue; + + public override int RandomSkinHue() => RandomUtils.Random(1755, 25) | 0x8000; + + public override int ClipHairHue(int hue) + { + for (var i = 0; i < m_HornHues.Length; i++) + { + if (m_HornHues[i] == hue) + { + return hue; + } + } + + return m_HornHues[0]; + } + + public override int RandomHairHue() => m_HornHues.RandomElement(); + } +} diff --git a/src/Prima.UOData/Packets/CharacterCreation.cs b/src/Prima.UOData/Packets/CharacterCreation.cs index 16a4607..bc97ae2 100644 --- a/src/Prima.UOData/Packets/CharacterCreation.cs +++ b/src/Prima.UOData/Packets/CharacterCreation.cs @@ -1,6 +1,6 @@ using Orion.Foundations.Spans; using Prima.Network.Packets.Base; - +using Prima.UOData.Context; using Prima.UOData.Data; using Prima.UOData.Types; @@ -27,6 +27,10 @@ public class CharacterCreation : BaseUoNetworkPacket public int Int { get; set; } + public bool IsFemale { get; set; } + + public int Hue { get; set; } + public short HairStyle { get; set; } public short HairColor { get; set; } @@ -37,6 +41,7 @@ public class CharacterCreation : BaseUoNetworkPacket public short StartingLocation { get; set; } + public Race Race { get; set; } public short ShirtColor { get; set; } @@ -64,9 +69,15 @@ public override void Read(SpanReader reader) LoginCount = reader.ReadInt32(); Profession = ProfessionInfo.Professions[reader.ReadByte()]; - reader.Read(new byte[15]); + reader.ReadBytes(15); + + var genderRace = reader.ReadByte(); + Sex = (SexType)genderRace; - Sex = (SexType)reader.ReadByte(); + IsFemale = genderRace % 2 != 0; + + var raceID = UOContext.StygianAbyss ? (byte)(genderRace < 4 ? 0 : genderRace / 2 - 1) : (byte)(genderRace / 2); + Race = Race.Races[raceID] ?? Race.DefaultRace; Str = reader.ReadByte(); Dex = reader.ReadByte(); @@ -80,6 +91,9 @@ public override void Read(SpanReader reader) Skills.Add(skillName, skillValue); } + + Hue = reader.ReadUInt16(); + HairStyle = reader.ReadInt16(); HairColor = reader.ReadInt16(); diff --git a/src/Prima.UOData/Services/ClientConfigurationService.cs b/src/Prima.UOData/Services/ClientConfigurationService.cs index 95b7367..a045516 100644 --- a/src/Prima.UOData/Services/ClientConfigurationService.cs +++ b/src/Prima.UOData/Services/ClientConfigurationService.cs @@ -52,6 +52,8 @@ public async Task HandleAsync(ServerStartedEvent @event, CancellationToken cance await GetExpansionAsync(); await LoadSkillInfoAsync(); await BuildProfessionsAsync(); + + RaceDefinitions.Configure(); } private async Task GetExpansionAsync() diff --git a/src/Prima.UOData/Utils/Utility.cs b/src/Prima.UOData/Utils/Utility.cs index 30ef5c2..83d0117 100644 --- a/src/Prima.UOData/Utils/Utility.cs +++ b/src/Prima.UOData/Utils/Utility.cs @@ -19,8 +19,6 @@ public static partial class Utility { private static Dictionary _ipAddressTable; - private static readonly Stack m_ConsoleColors = new(); - public static void Separate(StringBuilder sb, string value, string separator) { if (sb.Length > 0) @@ -281,4 +279,9 @@ public static void FixPoints(ref Point3D top, ref Point3D bottom) (top.m_Z, bottom.m_Z) = (bottom.m_Z, top.m_Z); } } + + public static T RandomList(params T[] array) + { + return array.ToList().RandomElement(); + } }