diff --git a/README.md b/README.md index bb38c02..9aabaab 100644 --- a/README.md +++ b/README.md @@ -51,23 +51,18 @@ The currently supported version of the client: **2.7.4.84161** ### Compile and run 1. Install [.NET 7 SDK and runtime](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) (just runtime, not asp.net or desktop) 2. Go to the repo directory and compile the project using this command: - ```shell - dotnet publish ./src/DiIiS-NA/Blizzless.csproj --configuration Release --output ./publish - ``` -3. [Skip this stage for local game] Copy the [config.ini](configs/config.ini) file to the publish folder (It overwrites the default settings): - - Update the parameter entries with your IP record on the network: `BindIP` and `PublicIP`. -4. Go to the publish folder, launch Blizzless executable, wait until server start - it creates a hierarchy. -5. Create user account(s) using console: `!account add Login Password Tag` - -#### Example: - -> !account add username@ YourPassword YourBattleTag - -Creates an account with Login `username@`, password `YourPassword` and BattleTag `YourBattleTag` - -> !account add username@ YourPassword YourBattleTag owner - -Creates an account with Login `username@`, password `YourPassword` and BattleTag `YourBattleTag` with rank `owner` +```shell +dotnet publish ./src/DiIiS-NA/Blizzless.csproj --configuration Release --output ./publish +``` +3. __Skip this stage for local game__ Copy the [config.mods.json](https://github.com/blizzless/blizzless-diiis/blob/community/configs/config.mods.json) file to the folder, and modify however you want. A file will be generated automatically from the `config.ini` for now. +4. Update your `config.ini` file on the published folder with your network's IP records (`BindIP` and `PublicIP`) +5. Go to the publish folder, launch Blizzless executable, wait until server start - it creates a hierarchy. +6. Create user account(s) using console: `!account add Login Password Tag` + - Example: + - `!account add username@ YourPassword YourBattleTag` + - Creates an account with Login `username@`, password `YourPassword` and BattleTag `YourBattleTag` + - `!account add username@ YourPassword YourBattleTag owner` + - Creates an account with Login `username@`, password `YourPassword` and BattleTag `YourBattleTag` with rank `owner` ## Prepare Client diff --git a/configs/config.ini b/configs/config.ini deleted file mode 100644 index 70c2f39..0000000 --- a/configs/config.ini +++ /dev/null @@ -1,114 +0,0 @@ -; -; # This is a template configuration file which can be modified as desired. -; -; # Community branch (recommended): https://github.com/blizzless/blizzless-diiis/tree/community -; # test-stable branch: https://github.com/blizzless/blizzless-diiis/ -; # Master branch: https://github.com/blizzless/blizzless-diiis/tree/master -; - -; Settings for Bnet -[Battle-Server] -Enabled = true -BindIP = 127.0.0.1 -WebPort = 9800 -Port = 1119 -MotdEnabled = true -Motd = Welcome to Blizzless D3! - -; ------------------------ -; [IWServer] -; IWServer = false - -; ------------------------ -; REST services for login (and others) -[REST] -IP = 127.0.0.1 -Public = true -PublicIP = 127.0.0.1 -PORT = 80 - -; ------------------------ -; Game server options and game-mods. -; -[Game-Server] -Enabled = true -CoreActive = true -BindIP = 127.0.0.1 -WebPort = 9001 -Port = 1345 -BindIPv6 = ::1 -DRLGemu = true - -; Modding of game (please check https://github.com/blizzless/blizzless-diiis/blob/community/docs/game-world-settings.md) -; - -; rates -RateExp = 1 -RateMoney = 1 -RateDrop = 1 -RateChangeDrop = 1 -RateMonsterHP = 1 -RateMonsterDMG = 1 -; items -ChanceHighQualityUnidentified = 30 -ChanceNormalUnidentified = 5 -; bosses -BossHealthMultiplier = 6 -BossDamageMultiplier = 3 -; nephalem -NephalemRiftProgressMultiplier = 1 -; health -HealthPotionRestorePercentage = 60 -HealthPotionCooldown = 30 -ResurrectionCharges = 3 -; waypoints -UnlockAllWaypoints = false -; player attribute modifier -StrengthMultiplier = 1 -StrengthParagonMultiplier = 1 -DexterityMultiplier = 1 -DexterityParagonMultiplier = 1 -IntelligenceMultiplier = 1 -IntelligenceParagonMultiplier = 1 -VitalityMultiplier = 1 -VitalityParagonMultiplier = 1 -; quests -AutoSaveQuests = false -; minimap -ForceMinimapVisibility = false - -; ------------------------ -; Network address translation -; -[NAT] -Enabled = True -; use your public IP -PublicIP = 127.0.0.1 - -; ------------------------ -; Where the outputs should be. -; Best for visualization (default): AnsiLog (target: Ansi) -; Best for debugging: ConsoleLog (target: console) -; Best for packet analysis: PacketLog (target: file) -; -[AnsiLog] -Enabled = true -Target = Ansi -IncludeTimeStamps = true -MinimumLevel = Debug -MaximumLevel = Fatal - -[ConsoleLog] -Enabled = false -Target = Console -IncludeTimeStamps = true -MinimumLevel = Debug -MaximumLevel = PacketDump - -[PacketLog] -Enabled = true -Target = file -FileName = packet.log -IncludeTimeStamps = true -MinimumLevel = Debug -MaximumLevel = PacketDump diff --git a/configs/config.mods.json b/configs/config.mods.json new file mode 100644 index 0000000..5341038 --- /dev/null +++ b/configs/config.mods.json @@ -0,0 +1,60 @@ +{ + "Rate": { + "Experience": 1.0, + "Money": 1.0, + "Drop": 1.0, + "ChangeDrop": 1.0 + }, + "Health": { + "PotionRestorePercentage": 60.0, + "PotionCooldown": 30.0, + "ResurrectionCharges": 3 + }, + "Monster": { + "HealthMultiplier": 1.0, + "DamageMultiplier": 1.0 + }, + "Boss": { + "HealthMultiplier": 6.0, + "DamageMultiplier": 3.0 + }, + "Quest": { + "AutoSave": false, + "UnlockAllWaypoints": false + }, + "Player": { + "Multipliers": { + "Strength": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Dexterity": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Intelligence": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Vitality": { + "Normal": 1.0, + "Paragon": 1.0 + } + } + }, + "Items": { + "UnidentifiedDropChances": { + "HighQuality": 30.0, + "NormalQuality": 5.0 + } + }, + "Minimap": { + "ForceVisibility": false + }, + "NephalemRift": { + "ProgressMultiplier": 1.0, + "AutoFinish": false, + "AutoFinishThreshold": 2, + "OrbsChance": 2.0 + } +} \ No newline at end of file diff --git a/docs/game-world-settings.md b/docs/game-world-settings.md index 8863def..0c60118 100644 --- a/docs/game-world-settings.md +++ b/docs/game-world-settings.md @@ -1,81 +1,76 @@ # Game World Settings -The parameters of the world can be easily altered using the configuration file located within `config.ini`. +The parameters of the world can be easily altered using the configuration file located within `config.maps.json`, which is built on server initialization. + +For older configs, it will be migrated from `config.ini` automatically. ## Configuration -The parameters specified in the `config.ini` file will be saved to the server folder, overwriting the default settings. For example, all values below use their default settings. +The parameters specified in the `config.mods.json` file will be created on the server folder, migrating from config.ini, to overwrite the default settings. For example, all values below use their default settings. -```ini -[Game-Server] -; rates -RateExp = 1 -RateMoney = 1 -RateDrop = 1 -RateChangeDrop = 1 -RateMonsterHP = 1 -RateMonsterDMG = 1 -; items -ChanceHighQualityUnidentified = 30 -ChanceNormalUnidentified = 5 -; bosses -BossHealthMultiplier = 6 -BossDamageMultiplier = 3 -; nephalem -NephalemRiftProgressMultiplier = 1 -NephalemRiftAutoFinish = false -NephalemRiftAutoFinishThreshold = 2 -NephalemRiftOrbsChance = 0 -; health -HealthPotionRestorePercentage = 60 -HealthPotionCooldown = 30 -ResurrectionCharges = 3 -; waypoints -UnlockAllWaypoints = false -; player attribute modifier -StrengthMultiplier = 1 -StrengthParagonMultiplier = 1 -DexterityMultiplier = 1 -DexterityParagonMultiplier = 1 -IntelligenceMultiplier = 1 -IntelligenceParagonMultiplier = 1 -VitalityMultiplier = 1 -VitalityParagonMultiplier = 1 -; quests -AutoSaveQuests = false -; minimap -ForceMinimapVisibility = false -``` +The default configuration can be found at [config.mods.json](https://github.com/blizzless/blizzless-diiis/blob/community/configs/config.mods.json) ## Description -| Key | Description | -| ---------------- | ------------------------- | -| `RateExp` | Experience multiplier | -| `RateMoney` | Currency multiplier | -| `RateDrop` | Drop quantity multiplier | -| `RateChangeDrop` | Drop quality multiplier | -| `RateMonsterHP` | Monsters HP multiplier | -| `RateMonsterDMG` | Monster damage multiplier | -| `ChanceHighQualityUnidentified` | Percentage that a unique, legendary, set or special item created is unidentified | -| `ChanceNormalUnidentified` | Percentage that normal item created is unidentified | -| `ResurrectionCharges` | Amount of times user can resurrect at corpse | -| `BossHealthMultiplier` | Boss Health Multiplier | -| `BossDamageMultiplier` | Boss Damage Multiplier | -| `HealthPotionRestorePercentage` | How much (from 1-100) a health potion will heal. | -| `HealthPotionCooldown` | How much (in seconds) to use a health potion again. | -| `UnlockAllWaypoints` | Unlocks all waypoints in campaign | -| `StrengthMultiplier` | Player's strength multiplier | -| `StrengthParagonMultiplier` | Player's strength multiplier **for paragons** | -| `DexterityMultiplier` | Player's dexterity multiplier | -| `DexterityParagonMultiplier` | Player's dexterity multiplier **for paragons** | -| `IntelligenceMultiplier` | Player's intelligence multiplier | -| `IntelligenceParagonMultiplier` | Player's intelligence multiplier **for paragons** | -| `VitalityMultiplier` | Player's vitality multiplier | -| `VitalityParagonMultiplier` | Player's vitality multiplier **for paragons** | -| `AutoSaveQuests` *in tests* | Force Save Quests/Step, even if Act's quest setup marked as Saveable = FALSE. Doesn't apply to OpenWorld games. | -| `NephalemRiftProgressMultiplier` | Nephalem Rift Progress Modifier | -| `NephalemRiftAutoFinish` | Nephalem Auto-Finish when there's still `NephalemRiftAutoFinishThreshold` monsters or less are alive on the rift | -| `NephalemRiftAutoFinishThreshold` | Nephalem Rift Progress Modifier | -| `NephalemRiftOrbsChance` | Nephalem Rifts chance of spawning a orb. | -| `ForceMinimapVisibility` | Forces the minimap visibility | +```json +{ + "Rate": { + "Experience": 1.0, // Experience Rate + "Money": 1.0, // money rate + "Drop": 1.0, // drop rate + "ChangeDrop": 1.0 // change drop rate + }, + "Health": { + "PotionRestorePercentage": 60.0, // how many in percent will a potion restore + "PotionCooldown": 30.0, // how many seconds for a full potion recharge + "ResurrectionCharges": 3 // how many times can you revive at corpse + }, + "Monster": { + "HealthMultiplier": 1.0, // monster health multiplier + "DamageMultiplier": 1.0 // monster damage multiplier + }, + "Boss": { + "HealthMultiplier": 6.0, // boss health multiplier + "DamageMultiplier": 3.0 // boss damage multiplier + }, + "Quest": { + "AutoSave": false, // auto save at every quest + "UnlockAllWaypoints": false // unlocks all waypoints in-game + }, + "Player": { + "Multipliers": { // multipliers for the player (e.g. a paragon might need twice these values for fairer gameplay) + "Strength": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Dexterity": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Intelligence": { + "Normal": 1.0, + "Paragon": 1.0 + }, + "Vitality": { + "Normal": 1.0, + "Paragon": 1.0 + } + } + }, + "Items": { + "UnidentifiedDropChances": { // chances in % of a dropped item to be unidentified + "HighQuality": 30.0, + "NormalQuality": 5.0 + } + }, + "Minimap": { + "ForceVisibility": false // forces in-game minimap to be always visible + }, + "NephalemRift": { // improves overall nephalem rift experience + "ProgressMultiplier": 1.0, + "AutoFinish": false, + "AutoFinishThreshold": 2, + "OrbsChance": 2.0 // chances of spawning an orb + } +} +``` diff --git a/src/DiIiS-NA/BGS-Server/Base/ConnectHandler.cs b/src/DiIiS-NA/BGS-Server/Base/ConnectHandler.cs index 1546a37..df4b1bb 100644 --- a/src/DiIiS-NA/BGS-Server/Base/ConnectHandler.cs +++ b/src/DiIiS-NA/BGS-Server/Base/ConnectHandler.cs @@ -250,6 +250,8 @@ namespace DiIiS_NA.LoginServer.Base } internal class WebSocketServerProtocolHandshakeHandler : ChannelHandlerAdapter { + + static readonly Logger _logger = LogManager.CreateLogger(); private readonly string websocketPath; private readonly string subprotocols; @@ -290,12 +292,16 @@ namespace DiIiS_NA.LoginServer.Base { if (!object.Equals(req.Method, HttpMethod.Get)) { - SendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.Forbidden)); + SendHttpResponse(ctx, req, + new DefaultFullHttpResponse(HttpVersion.Http11, HttpResponseStatus.Forbidden)); return; } + //v1.rpc.battle.net // - WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory(GetWebSocketLocation(ctx.Channel.Pipeline, req, websocketPath), subprotocols, allowExtensions, maxFramePayloadSize, allowMaskMismatch); + WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = + new WebSocketServerHandshakerFactory(GetWebSocketLocation(ctx.Channel.Pipeline, req, websocketPath), + subprotocols, allowExtensions, maxFramePayloadSize, allowMaskMismatch); WebSocketServerHandshaker handshaker = webSocketServerHandshakerFactory.NewHandshaker(req); if (handshaker == null) { @@ -303,7 +309,7 @@ namespace DiIiS_NA.LoginServer.Base return; } - handshaker.HandshakeAsync(ctx.Channel, req).ContinueWith(delegate (Task t) + handshaker.HandshakeAsync(ctx.Channel, req).ContinueWith(delegate(Task t) { if (t.Status != TaskStatus.RanToCompletion) { @@ -311,12 +317,17 @@ namespace DiIiS_NA.LoginServer.Base } else { - ctx.FireUserEventTriggered(new HandshakeHandler.HandshakeComplete(req.Uri, req.Headers, handshaker.SelectedSubprotocol)); + ctx.FireUserEventTriggered(new HandshakeHandler.HandshakeComplete(req.Uri, req.Headers, + handshaker.SelectedSubprotocol)); } }, TaskContinuationOptions.ExecuteSynchronously); HandshakeHandler.SetHandshaker(ctx.Channel, handshaker); ctx.Channel.Pipeline.Replace(this, "WS403Responder", HandshakeHandler.ForbiddenHttpRequestResponder()); } + catch (Exception ex) + { + _logger.ErrorException(ex, "Handshake failure"); + } finally { req.Release(); diff --git a/src/DiIiS-NA/BGS-Server/Battle/BattleClient.cs b/src/DiIiS-NA/BGS-Server/Battle/BattleClient.cs index dfc604d..2610b82 100644 --- a/src/DiIiS-NA/BGS-Server/Battle/BattleClient.cs +++ b/src/DiIiS-NA/BGS-Server/Battle/BattleClient.cs @@ -16,6 +16,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; using System.Net.Security; using System.Threading.Tasks; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Text; @@ -501,10 +503,41 @@ namespace DiIiS_NA.LoginServer.Battle } public void SendMotd() { - if (string.IsNullOrWhiteSpace(LoginServerConfig.Instance.Motd) || !LoginServerConfig.Instance.MotdEnabled) - return; - Logger.Debug($"Motd sent to {Account.BattleTag}."); - SendServerWhisper(LoginServerConfig.Instance.Motd); + if (LoginServerConfig.Instance.MotdEnabled) + { + if (LoginServerConfig.Instance.MotdEnabledRemote) + { + if (string.IsNullOrWhiteSpace(LoginServerConfig.Instance.MotdRemoteUrl)) + { + Logger.Warn("No Motd remote URL defined, falling back to normal motd."); + } + else + { + var url = LoginServerConfig.Instance.MotdRemoteUrl.Trim(); + HttpClient client = new(); + var post = client.PostAsJsonAsync(url, new + { + GameAccountId = InGameClient.Player?.Toon?.GameAccountId ?? 0, + ToonName = InGameClient.Player?.Toon?.Name ?? string.Empty, + WorldGlobalId = InGameClient.Player?.World?.GlobalID ?? 0 + }).Result; + if (post.IsSuccessStatusCode) + { + var text = post.Content.ReadAsStringAsync().Result; + SendServerWhisper(text); + Logger.Info("Remote Motd sent successfully."); + return; + } + + Logger.Warn("Could not POST to $[red]$" + url + "$[/]$. Please ensure the URL is correct. Falling back to normal MotD if available."); + } + } + if (!string.IsNullOrWhiteSpace(LoginServerConfig.Instance.Motd)) + { + Logger.Debug($"Motd sent to {Account.BattleTag}."); + SendServerWhisper(LoginServerConfig.Instance.Motd); + } + } } public override void ChannelInactive(IChannelHandlerContext context) diff --git a/src/DiIiS-NA/BGS-Server/LoginServerConfig.cs b/src/DiIiS-NA/BGS-Server/LoginServerConfig.cs index 40649bf..c0b53b8 100644 --- a/src/DiIiS-NA/BGS-Server/LoginServerConfig.cs +++ b/src/DiIiS-NA/BGS-Server/LoginServerConfig.cs @@ -47,6 +47,12 @@ namespace DiIiS_NA.LoginServer get => GetBoolean(nameof(MotdEnabled), true); set => Set(nameof(MotdEnabled), value); } + + public bool MotdEnabledWhenWorldLoads + { + get => GetBoolean(nameof(MotdEnabledWhenWorldLoads), false); + set => Set(nameof(MotdEnabledWhenWorldLoads), value); + } /// /// Motd text @@ -58,6 +64,18 @@ namespace DiIiS_NA.LoginServer set => Set(nameof(Motd), value); } + public bool MotdEnabledRemote + { + get => GetBoolean(nameof(MotdEnabledRemote), false); + set => Set(nameof(MotdEnabledRemote), value); + } + + public string MotdRemoteUrl + { + get => GetString(nameof(MotdRemoteUrl), ""); + set => Set(nameof(MotdRemoteUrl), value); + } + public static readonly LoginServerConfig Instance = new(); private LoginServerConfig() : base("Battle-Server") diff --git a/src/DiIiS-NA/Core/Extensions/DataConversionExtensions.cs b/src/DiIiS-NA/Core/Extensions/DataConversionExtensions.cs new file mode 100644 index 0000000..1bf1357 --- /dev/null +++ b/src/DiIiS-NA/Core/Extensions/DataConversionExtensions.cs @@ -0,0 +1,11 @@ +using System; + +namespace DiIiS_NA.Core.Extensions; + +public static class MathConversionsOperations +{ + public static int Floor(this float value) => (int) Math.Floor(value); + public static int Ceiling(this float value) => (int) Math.Ceiling(value); + public static int Floor(this double value) => (int) Math.Floor(value); + public static int Ceil(this double value) => (int) Math.Floor(value); +} \ No newline at end of file diff --git a/src/DiIiS-NA/Core/Logging/AnsiTarget.cs b/src/DiIiS-NA/Core/Logging/AnsiTarget.cs index bb93ea8..5a99285 100644 --- a/src/DiIiS-NA/Core/Logging/AnsiTarget.cs +++ b/src/DiIiS-NA/Core/Logging/AnsiTarget.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -24,12 +26,12 @@ public class AnsiTarget : LogTarget _table = new Table().Expand().ShowFooters().ShowHeaders().Border(TableBorder.Rounded); if (IncludeTimeStamps) - _table.AddColumn("Time"); + _table.AddColumn("Time").Centered(); _table - .AddColumn("Level") - .AddColumn("Logger") - .AddColumn("Message") - .AddColumn("Error"); + .AddColumn("Level").RightAligned() + .AddColumn("Message").Centered() + .AddColumn("Logger").LeftAligned() + .AddColumn("Error").RightAligned(); AnsiConsole.Live(_table).StartAsync(async ctx => { @@ -101,6 +103,15 @@ public class AnsiTarget : LogTarget } + private static Dictionary _replacements = new () + { + ["["] = "[[", + ["]"] = "]]", + ["$[[/]]$"] = "[/]", + ["$[["] = "[", + ["]]$"] = "]" + }; + /// /// Performs a cleanup on the target. /// All [ becomes [[, and ] becomes ]] (for ignoring ANSI codes) @@ -112,124 +123,85 @@ public class AnsiTarget : LogTarget /// /// /// - public static string Cleanup(string x) => Beautify(x.Replace("[", "[[").Replace("]", "]]").Replace("$[[/]]$", "[/]").Replace("$[[", "[").Replace("]]$", "]")); - + public static string Cleanup(string input) + { + if (string.IsNullOrEmpty(input)) return ""; + return Beautify(_replacements.Aggregate(input, (current, replacement) => current.Replace(replacement.Key, replacement.Value))); + } public override void LogMessage(Logger.Level level, string logger, string message) { - if (CancellationTokenSource.IsCancellationRequested) - return; + if (CancellationTokenSource.IsCancellationRequested) return; + try { - if (IncludeTimeStamps) - _table.AddRow( - new Markup(DateTime.Now.ToString(TimeStampFormat), GetStyleByLevel(level)), - new Markup(level.ToString(), GetStyleByLevel(level)).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup("", new Style(foreground: Color.Green3_1)).Centered()); - else - _table.AddRow( - new Markup(level.ToString()).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup("", new Style(foreground: Color.Green3_1)).Centered()); + AddRow(level, logger, message, ""); } catch (Exception ex) { - var regex = new Regex(@"\$\[.*?\]\$"); - var matches = regex.Matches(message); - foreach (Match match in matches) - { - message = message.Replace(match.Value, ""); - } - - if (IncludeTimeStamps) - { - _table.AddRow( - new Markup(DateTime.Now.ToString(TimeStampFormat), GetStyleByLevel(level)), - new Markup(level.ToString(), GetStyleByLevel(level)).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup(ex.Message, new Style(foreground: Color.Red3_1)).Centered()); - } - else - { - _table.AddRow( - new Markup(level.ToString()).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup(ex.Message, new Style(foreground: Color.Red3_1)).Centered()); - } + AddRow(level, logger, Cleanup(StripMarkup(message)), ex.Message, true); } } public override void LogException(Logger.Level level, string logger, string message, Exception exception) { - if (CancellationTokenSource.IsCancellationRequested) - return; + if (CancellationTokenSource.IsCancellationRequested) return; + try { - if (IncludeTimeStamps) - _table.AddRow( - new Markup(DateTime.Now.ToString(TimeStampFormat), GetStyleByLevel(level)), - new Markup(level.ToString(), GetStyleByLevel(level)).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup( - $"[underline red3_1 on white]{exception.GetType().Name}[/]\n" + Cleanup(exception.Message), - new Style(foreground: Color.Red3_1)).Centered()); - else - _table.AddRow( - new Markup(level.ToString()).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(message, GetStyleByLevel(level)).LeftJustified(), - new Markup( - $"[underline red3_1 on white]{exception.GetType().Name}[/]\n" + Cleanup(exception.Message), - new Style(foreground: Color.Red3_1)).Centered()); + AddRow(level, logger, message, $"[underline red3_1 on white]{exception.GetType().Name}[/]\n" + Cleanup(exception.Message), exFormat: true); } catch (Exception ex) { - var regex = new Regex(@"\$\[.*?\]\$"); - var matches = regex.Matches(message); - foreach (Match match in matches) - { - message = message.Replace(match.Value, ""); - } - - if (IncludeTimeStamps) - { - _table.AddRow( - new Markup(DateTime.Now.ToString(TimeStampFormat), GetStyleByLevel(level)), - new Markup(level.ToString(), GetStyleByLevel(level)).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup(ex.Message, new Style(foreground: Color.Red3_1)).Centered()); - } - else - { - _table.AddRow( - new Markup(level.ToString()).RightJustified(), - new Markup(logger, GetStyleByLevel(level)).LeftJustified(), - new Markup(Cleanup(message), GetStyleByLevel(level)).LeftJustified(), - new Markup(ex.Message, new Style(foreground: Color.Red3_1)).Centered()); - } + AddRow(level, logger, Cleanup(StripMarkup(message)), ex.Message, true); } -} + } + + private void AddRow(Logger.Level level, string logger, string message, string exMessage, bool isError = false, + bool exFormat = false) + { + Style messageStyle = GetStyleByLevel(level); + Style exStyle = exFormat ? new Style(foreground: Color.Red3_1) : new Style(foreground: Color.Green3_1); + var colTimestamp = new Markup(DateTime.Now.ToString(TimeStampFormat), messageStyle).Centered(); + var colLevel = new Markup(level.ToString(), messageStyle).RightJustified(); + var colMessage = new Markup(Cleanup(message), messageStyle).Centered(); + var colLogger = new Markup(logger, new Style(messageStyle.Foreground, messageStyle.Background, messageStyle.Decoration + #if DEBUG + //, link = ... + #endif + )).LeftJustified(); + var colError = new Markup(isError ? exMessage : "", exStyle).RightJustified(); + if (IncludeTimeStamps) _table.AddRow(colTimestamp, colLevel, colMessage, colLogger, colError); + else _table.AddRow(colLevel, colMessage, colLogger, colError); + } + + private string StripMarkup(string message) + { + var regex = new Regex(@"\$\[.*?\]\$"); + var matches = regex.Matches(message); + foreach (Match match in matches) + { + message = message.Replace(match.Value, ""); + } + + return message; + } private static Style GetStyleByLevel(Logger.Level level) { return level switch { - Logger.Level.RenameAccountLog => new Style(Color.DarkSlateGray3),// - Logger.Level.ChatMessage => new Style(Color.DarkSlateGray2),// - Logger.Level.Debug => new Style(Color.Olive),// - Logger.Level.MethodTrace => new Style(Color.DarkOliveGreen1_1),// - Logger.Level.Trace => new Style(Color.BlueViolet),// - Logger.Level.Info => new Style(Color.White), - Logger.Level.Success => new Style(Color.Green3_1), - Logger.Level.Warn => new Style(Color.Yellow),// + Logger.Level.RenameAccountLog => new Style(Color.Gold1),// + Logger.Level.ChatMessage => new Style(Color.Plum2),// + Logger.Level.Debug => new Style(Color.Grey62),// + Logger.Level.MethodTrace => new Style(Color.Grey74, decoration: Decoration.Dim | Decoration.Italic),// + Logger.Level.Trace => new Style(Color.Grey82),// + Logger.Level.Info => new Style(Color.SteelBlue), + Logger.Level.Success => new Style(Color.DarkOliveGreen3_2), + Logger.Level.Warn => new Style(Color.DarkOrange),// Logger.Level.Error => new Style(Color.IndianRed1),// Logger.Level.Fatal => new Style(Color.Red3_1),// + Logger.Level.QuestInfo => new Style(Color.Plum2), + Logger.Level.QuestStep => new Style(Color.Plum3, decoration: Decoration.Dim), Logger.Level.PacketDump => new Style(Color.Maroon),// _ => new Style(Color.White) }; @@ -242,4 +214,7 @@ public static class AnsiTargetExtensions { return text.Replace("$[", "").Replace("]$", "").Replace("[", "[[").Replace("]", "]]"); } + + public static string StyleAnsi(this object obj, string style) => + $"$[{style}]$" + obj.ToString().EscapeMarkup() + "$[/]$"; } \ No newline at end of file diff --git a/src/DiIiS-NA/Core/Logging/LogManager.cs b/src/DiIiS-NA/Core/Logging/LogManager.cs index dc3e7eb..6bca5c9 100644 --- a/src/DiIiS-NA/Core/Logging/LogManager.cs +++ b/src/DiIiS-NA/Core/Logging/LogManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace DiIiS_NA.Core.Logging { @@ -25,7 +26,7 @@ namespace DiIiS_NA.Core.Logging /// Creates and returns a logger named with declaring type. /// /// A instance. - public static Logger CreateLogger() + public static Logger CreateLogger([CallerFilePath] string filePath = "") { var frame = new StackFrame(1, false); // read stack frame. var name = frame.GetMethod().DeclaringType.Name; // get declaring type's name. @@ -33,7 +34,7 @@ namespace DiIiS_NA.Core.Logging if (name == null) // see if we got a name. throw new Exception("Error getting full name for declaring type."); - return CreateLogger(name); // return the newly created logger. + return CreateLogger(name, filePath); // return the newly created logger. } /// @@ -41,10 +42,10 @@ namespace DiIiS_NA.Core.Logging /// /// /// A instance. - public static Logger CreateLogger(string name) + public static Logger CreateLogger(string name, [CallerFilePath] string filePath = "") { if (!Loggers.ContainsKey(name)) // see if we already have instance for the given name. - Loggers.Add(name, new Logger(name)); // add it to dictionary of loggers. + Loggers.Add(name, new Logger(name, filePath)); // add it to dictionary of loggers. return Loggers[name]; // return the newly created logger. } diff --git a/src/DiIiS-NA/Core/Logging/Logger.cs b/src/DiIiS-NA/Core/Logging/Logger.cs index 8e3a012..2b6fa32 100644 --- a/src/DiIiS-NA/Core/Logging/Logger.cs +++ b/src/DiIiS-NA/Core/Logging/Logger.cs @@ -13,15 +13,17 @@ namespace DiIiS_NA.Core.Logging public class Logger { public string Name { get; protected set; } + public string FilePath { get; protected set; } /// /// A logger base type is used to create a logger instance. /// E.g. ConsoleTarget, FileTarget, etc. /// /// Logger name - public Logger(string name) + public Logger(string name, string filePath = null) { Name = name; + FilePath = filePath; } public enum Level @@ -66,6 +68,15 @@ namespace DiIiS_NA.Core.Logging /// Fatal messages (usually unrecoverable errors that leads to client or server crashes). /// Fatal, + + /// + /// The messages meant for quest general logging purposes. + /// + QuestInfo, + /// + /// The messages meant for quest logging purposes. + /// + QuestStep, /// /// Packet messages. /// @@ -105,14 +116,39 @@ namespace DiIiS_NA.Core.Logging /// The log message. public void MethodTrace(string message, [CallerMemberName] string methodName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) { + var m = $"$[darkolivegreen3_2]${methodName}()$[/]$"; #if DEBUG var fileName = Path.GetFileName(filePath); - Log(Level.MethodTrace, $"$[underline white]${fileName}:{lineNumber}$[/]$ $[darkolivegreen3_2]${methodName}()$[/]$: " + message, null); + Log(Level.MethodTrace, $"$[red]${fileName}:{lineNumber}$[/]$ in {m}: " + message, null); #else - Log(Level.MethodTrace, $"$[darkolivegreen3_2]${methodName}()$[/]$: " + message, null); + Log(Level.MethodTrace, $"{m}: " + message, null); #endif } + public void QuestStep(string message, [CallerMemberName] string methodName = "", + [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) + { + var m = $"$[darkolivegreen3_2]${methodName}()$[/]$"; +#if DEBUG + var fileName = Path.GetFileName(filePath); + Log(Level.MethodTrace, $"$[red]${fileName}:{lineNumber}$[/]$ in {m}: " + message, null); +#else + Log(Level.MethodTrace, $"{m}: " + message, null); +#endif + } + + public void QuestInfo(string message, [CallerMemberName] string methodName = "", + [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) + { + var m = $"$[darkolivegreen3_2]${methodName}()$[/]$"; +#if DEBUG + var fileName = Path.GetFileName(filePath); + Log(Level.MethodTrace, $"$[red]${fileName}:{lineNumber}$[/]$ in {m}: " + message, null); +#else + Log(Level.MethodTrace, $"{m}: " + message, null); +#endif + } + /// The log message. public void Debug(string message) => Log(Level.Debug, message, null); @@ -153,7 +189,7 @@ namespace DiIiS_NA.Core.Logging /// The log message. /// Additional arguments. - public void Fatal(string message, params object[] args) => Log(Level.Fatal, message, args); + public void Fatal(string message, params object[] args) => Log(Level.Fatal, message, args); #endregion diff --git a/src/DiIiS-NA/Core/MPQ/Data.cs b/src/DiIiS-NA/Core/MPQ/Data.cs index 2b46703..e0defcd 100644 --- a/src/DiIiS-NA/Core/MPQ/Data.cs +++ b/src/DiIiS-NA/Core/MPQ/Data.cs @@ -79,7 +79,7 @@ namespace DiIiS_NA.Core.MPQ public void Init() { - Logger.Info("Loading Diablo III Assets.."); + Logger.Info("Loading Diablo III Assets..."); DictSNOAccolade = Dicts.LoadAccolade(); DictSNOAct = Dicts.LoadActs(); DictSNOActor = Dicts.LoadActors(); diff --git a/src/DiIiS-NA/Core/MPQ/MPQPatchChain.cs b/src/DiIiS-NA/Core/MPQ/MPQPatchChain.cs index 3a9040b..0f5c1a7 100644 --- a/src/DiIiS-NA/Core/MPQ/MPQPatchChain.cs +++ b/src/DiIiS-NA/Core/MPQ/MPQPatchChain.cs @@ -30,11 +30,11 @@ namespace DiIiS_NA.Core.MPQ var mpqFile = MPQStorage.GetMPQFile(file); if (mpqFile == null) { - Logger.Error("Cannot find base MPQ file: {0}.", file); + Logger.Fatal("Cannot find base MPQ file: $[white on red]${0}$[/]$.", file); return; } this.BaseMPQFiles.Add(mpqFile); - Logger.Trace("Added MPQ storage: {0}.", file); + Logger.Debug($"Added MPQ storage: $[white underline]${file}$[/]$."); } this.PatchPattern = patchPattern; @@ -45,7 +45,7 @@ namespace DiIiS_NA.Core.MPQ this.Loaded = true; else { - Logger.Error("Required patch-chain version {0} is not satified (found version: {1}).", this.RequiredVersion, topMostMPQVersion); + Logger.Error("Required patch-chain version {0} is not satisfied (found version: {1}).", this.RequiredVersion, topMostMPQVersion); } } diff --git a/src/DiIiS-NA/D3-GameServer/CommandManager/CommandManager.cs b/src/DiIiS-NA/D3-GameServer/CommandManager/CommandManager.cs index 56f648a..7b9c75c 100644 --- a/src/DiIiS-NA/D3-GameServer/CommandManager/CommandManager.cs +++ b/src/DiIiS-NA/D3-GameServer/CommandManager/CommandManager.cs @@ -119,7 +119,7 @@ namespace DiIiS_NA.GameServer.CommandManager output = $"Unknown command."; #endif - if (output == string.Empty) + if (string.IsNullOrEmpty(output)) return true; if (output.Contains("\n")) diff --git a/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/GameCommand.cs b/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/GameCommand.cs new file mode 100644 index 0000000..367897e --- /dev/null +++ b/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/GameCommand.cs @@ -0,0 +1,18 @@ +using DiIiS_NA.Core.Logging; +using DiIiS_NA.D3_GameServer; +using DiIiS_NA.LoginServer.AccountsSystem; +using DiIiS_NA.LoginServer.Battle; + +namespace DiIiS_NA.GameServer.CommandManager; + +[CommandGroup("game", "Game Commands", Account.UserLevels.Admin, inGameOnly: false)] +public class GameCommand : CommandGroup +{ + private static readonly Logger Logger = LogManager.CreateLogger(); + [Command("reload-mods", "Reload all game mods file.", Account.UserLevels.Admin, inGameOnly: false)] + public void ReloadMods(string[] @params, BattleClient invokerClient) + { + GameModsConfig.ReloadSettings(); + invokerClient?.SendServerWhisper("Game mods updated successfully!"); + } +} \ No newline at end of file diff --git a/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/UnlockArtCommand.cs b/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/UnlockArtCommand.cs index a24cd2a..25f5967 100644 --- a/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/UnlockArtCommand.cs +++ b/src/DiIiS-NA/D3-GameServer/CommandManager/Commands/UnlockArtCommand.cs @@ -1,4 +1,5 @@ -using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Hireling; +using System; +using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Hireling; using DiIiS_NA.LoginServer.AccountsSystem; using DiIiS_NA.LoginServer.Battle; diff --git a/src/DiIiS-NA/D3-GameServer/Core/Types/Math/Vector3D.cs b/src/DiIiS-NA/D3-GameServer/Core/Types/Math/Vector3D.cs index d271cf4..5c68e74 100644 --- a/src/DiIiS-NA/D3-GameServer/Core/Types/Math/Vector3D.cs +++ b/src/DiIiS-NA/D3-GameServer/Core/Types/Math/Vector3D.cs @@ -112,6 +112,24 @@ namespace DiIiS_NA.GameServer.Core.Types.Math return ((x * x) + (y * y)) + (z * z); } + private static Random rand = new Random(); + + public Vector3D Around(float radius) + { + return Around(radius, radius, radius); + } + public Vector3D Around(float x, float y, float z) + { + float newX = X + ((float)rand.NextDouble() * 2 * x) - x; + float newY = Y + ((float)rand.NextDouble() * 2 * y) - y; + float newZ = Z + ((float)rand.NextDouble() * 2 * z) - z; + return new Vector3D(newX, newY, newZ); + } + + public Vector3D Around(Vector3D vector) + { + return Around(vector.X, vector.Y, vector.Z); + } public static bool operator ==(Vector3D a, Vector3D b) => a?.Equals(b) ?? ReferenceEquals(null, b); diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brain.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brain.cs index a0d46dd..2e546c2 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brain.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brain.cs @@ -47,7 +47,7 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem public virtual void Update(int tickCounter) { - if (State == BrainState.Dead || Body == null || Body.World == null || State == BrainState.Off) + if (State == BrainState.Dead || Body?.World == null || State == BrainState.Off) return; Think(tickCounter); // let the brain think. diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brains/MonsterBrain.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brains/MonsterBrain.cs index 6ea3910..345137b 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brains/MonsterBrain.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/AISystem/Brains/MonsterBrain.cs @@ -21,456 +21,532 @@ using DiIiS_NA.GameServer.MessageSystem; namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains { - public class MonsterBrain : Brain - { - private new readonly Logger Logger; - // list of power SNOs that are defined for the monster - public Dictionary PresetPowers { get; private set; } + public class MonsterBrain : Brain + { + private new readonly Logger _logger; - private TickTimer _powerDelay; + // list of power SNOs that are defined for the monster + public Dictionary PresetPowers { get; private set; } - public struct Cooldown - { - public TickTimer CooldownTimer; - public float CooldownTime; - } + private TickTimer _powerDelay; - private bool _warnedNoPowers; - private Actor _target { get; set; } - private int _mpqPowerCount; - private bool Feared = false; + public struct Cooldown + { + public TickTimer CooldownTimer; + public float CooldownTime; + } - public Actor AttackedBy = null; - public TickTimer TimeoutAttacked = null; + private bool _warnedNoPowers; + private Actor Target { get; set; } + private int _mpqPowerCount; + private bool _feared = false; - public Actor PriorityTarget = null; + public Actor AttackedBy = null; + public TickTimer TimeoutAttacked = null; - public MonsterBrain(Actor body) - : base(body) - { - Logger = LogManager.CreateLogger(GetType().Name); - - PresetPowers = new Dictionary(); + public Actor PriorityTarget = null; - // build list of powers defined in monster mpq data - if (body.ActorData.MonsterSNO <= 0) - { - Logger.Warn($"$[red]${GetType().Name}$[/]$ - Monster \"{body.SNO}\" has no monster SNO"); - return; - } - var monsterData = (DiIiS_NA.Core.MPQ.FileFormats.Monster)MPQStorage.Data.Assets[SNOGroup.Monster][body.ActorData.MonsterSNO].Data; - _mpqPowerCount = monsterData.SkillDeclarations.Count(e => e.SNOPower != -1); - for (int i = 0; i < monsterData.SkillDeclarations.Length; i++) - { - if (monsterData.SkillDeclarations[i].SNOPower == -1) continue; - if (PowerLoader.HasImplementationForPowerSNO(monsterData.SkillDeclarations[i].SNOPower)) - { - var cooldownTime = monsterData.MonsterSkillDeclarations[i].Timer / 10f; - PresetPowers.Add(monsterData.SkillDeclarations[i].SNOPower, new Cooldown { CooldownTimer = null, CooldownTime = cooldownTime }); - } - } + public MonsterBrain(Actor body) + : base(body) + { + _logger = LogManager.CreateLogger(GetType().Name); - if (monsterData.SkillDeclarations.All(s => s.SNOPower != 30592)) - PresetPowers.Add(30592, new Cooldown { CooldownTimer = null, CooldownTime = 0f }); //hack for dummy mobs without powers - } + PresetPowers = new Dictionary(); - public override void Think(int tickCounter) - { - switch (Body.SNO) - { - case ActorSno._uber_siegebreakerdemon: - case ActorSno._a4dun_garden_corruption_monster: - case ActorSno._a4dun_garden_hellportal_pillar: - case ActorSno._belialvoiceover: - return; - } + // build list of powers defined in monster mpq data + if (body.ActorData.MonsterSNO <= 0) + { + _logger.Warn($"$[red]${GetType().Name}$[/]$ - Monster \"{body.SNO}\" has no monster SNO"); + return; + } - if (Body.Hidden) - return; + var monsterData = + (DiIiS_NA.Core.MPQ.FileFormats.Monster)MPQStorage.Data.Assets[SNOGroup.Monster][ + body.ActorData.MonsterSNO].Data; + _mpqPowerCount = monsterData.SkillDeclarations.Count(e => e.SNOPower != -1); + for (int i = 0; i < monsterData.SkillDeclarations.Length; i++) + { + if (monsterData.SkillDeclarations[i].SNOPower == -1) continue; + if (!PowerLoader.HasImplementationForPowerSNO(monsterData.SkillDeclarations[i].SNOPower)) continue; + var cooldownTime = monsterData.MonsterSkillDeclarations[i].Timer / 10f; + PresetPowers.Add(monsterData.SkillDeclarations[i].SNOPower, + new Cooldown { CooldownTimer = null, CooldownTime = cooldownTime }); + } - if (CurrentAction != null && PriorityTarget != null && PriorityTarget.Attributes[GameAttributes.Is_Helper] == true) - { - PriorityTarget = null; - CurrentAction.Cancel(tickCounter); - CurrentAction = null; - return; - } + if (monsterData.SkillDeclarations.All(s => s.SNOPower != 30592)) + PresetPowers.Add(30592, + new Cooldown { CooldownTimer = null, CooldownTime = 0f }); //hack for dummy mobs without powers + } - if (tickCounter % 60 != 0) return; - - if (Body is NPC) return; + public override void Think(int tickCounter) + { + switch (Body.SNO) + { + case ActorSno._uber_siegebreakerdemon: + case ActorSno._a4dun_garden_corruption_monster: + case ActorSno._a4dun_garden_hellportal_pillar: + case ActorSno._belialvoiceover: + return; + } - if (!Body.Visible || Body.Dead) return; + if (Body.Hidden) + return; - if (Body.World.Game.Paused) return; - if (Body.Attributes[GameAttributes.Disabled]) return; + if (CurrentAction != null && PriorityTarget != null && + PriorityTarget.Attributes[GameAttributes.Is_Helper] == true) + { + PriorityTarget = null; + CurrentAction.Cancel(tickCounter); + CurrentAction = null; + return; + } - if (Body.Attributes[GameAttributes.Frozen] || - Body.Attributes[GameAttributes.Stunned] || - Body.Attributes[GameAttributes.Blind] || - Body.Attributes[GameAttributes.Webbed] || - Body.Disable || - Body.World.BuffManager.GetFirstBuff(Body) != null || - Body.World.BuffManager.GetFirstBuff(Body) != null) - { - if (CurrentAction != null) - { - CurrentAction.Cancel(tickCounter); - CurrentAction = null; - } - _powerDelay = null; + if (tickCounter % 60 != 0) return; - return; - } + if (Body is NPC) return; - if (Body.Attributes[GameAttributes.Feared]) - { - if (!Feared || CurrentAction == null) - { - if (CurrentAction != null) - { - CurrentAction.Cancel(tickCounter); - CurrentAction = null; - } - Feared = true; - CurrentAction = new MoveToPointWithPathfindAction( - Body, - PowerContext.RandomDirection(Body.Position, 3f, 8f) - ); - return; - } + if (!Body.Visible || Body.Dead) return; - return; - } + if (Body.World.Game.Paused) return; + if (Body.Attributes[GameAttributes.Disabled]) return; - Feared = false; + if (Body.Attributes[GameAttributes.Frozen] || + Body.Attributes[GameAttributes.Stunned] || + Body.Attributes[GameAttributes.Blind] || + Body.Attributes[GameAttributes.Webbed] || + Body.Disable || + Body.World.BuffManager.GetFirstBuff(Body) != null || + Body.World.BuffManager.GetFirstBuff(Body) != null) + { + if (CurrentAction != null) + { + CurrentAction.Cancel(tickCounter); + CurrentAction = null; + } - if (CurrentAction == null) - { - _powerDelay ??= new SecondsTickTimer(Body.World.Game, 1.0f); - if (AttackedBy != null || Body.GetObjectsInRange(50f).Count != 0) - { - if (_powerDelay.TimedOut) - { - _powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f); + _powerDelay = null; - if (AttackedBy != null) - PriorityTarget = AttackedBy; + return; + } - if (PriorityTarget == null) - { - Actor[] targets; + if (Body.Attributes[GameAttributes.Feared]) + { + if (_feared && CurrentAction != null) return; + if (CurrentAction != null) + { + CurrentAction.Cancel(tickCounter); + CurrentAction = null; + } - if (Body.Attributes[GameAttributes.Team_Override] == 1) - targets = Body.GetObjectsInRange(60f) - .Where(p => !p.Dead) - .OrderBy((monster) => PowerMath.Distance2D(monster.Position, Body.Position)) - .ToArray(); - else - targets = Body.GetActorsInRange(50f) - .Where(p => ((p is Player) && !p.Dead && p.Attributes[GameAttributes.Loading] == false && p.Attributes[GameAttributes.Is_Helper] == false && p.World.BuffManager.GetFirstBuff(p) == null) - || ((p is Minion) && !p.Dead && p.Attributes[GameAttributes.Is_Helper] == false) - || (p is DesctructibleLootContainer && p.SNO.IsDoorOrBarricade()) - || ((p is Hireling) && !p.Dead) - ) - .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) - .ToArray(); + _feared = true; + CurrentAction = new MoveToPointWithPathfindAction( + Body, + PowerContext.RandomDirection(Body.Position, 3f, 8f) + ); + return; + } - if (targets.Length == 0) return; - - _target = targets.First(); - } - else - _target = PriorityTarget; + _feared = false; - int powerToUse = PickPowerToUse(); - if (powerToUse > 0) - { - PowerScript power = PowerLoader.CreateImplementationForPowerSNO(powerToUse); - power.User = Body; - float attackRange = Body.ActorData.Cylinder.Ax2 + (power.EvalTag(PowerKeys.AttackRadius) > 0f ? (powerToUse == 30592 ? 10f : Math.Min((float)power.EvalTag(PowerKeys.AttackRadius), 35f)) : 35f); - float targetDistance = PowerMath.Distance2D(_target.Position, Body.Position); - if (targetDistance < attackRange + _target.ActorData.Cylinder.Ax2) - { - if (Body.WalkSpeed != 0) - Body.TranslateFacing(_target.Position, false); + if (CurrentAction != null) return; + _powerDelay ??= new SecondsTickTimer(Body.World.Game, 1.0f); + // Check if the character has been attacked or if there are any players within 50 units range + if (AttackedBy != null || Body.GetObjectsInRange(50f).Count != 0) + { + // If the power delay hasn't timed out, return + if (!_powerDelay.TimedOut) return; - CurrentAction = new PowerAction(Body, powerToUse, _target); + // Reset the power delay + _powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f); - if (power is SummoningSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) }; + // If the character has been attacked, set the attacker as the priority target + if (AttackedBy != null) + PriorityTarget = AttackedBy; - if (power is MonsterAffixSkill monsterAffixSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime }; + // If there's no defined priority target, start a search + if (PriorityTarget == null) + { + Actor[] targets; - if (PresetPowers[powerToUse].CooldownTime > 0f) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime }; + // If the character is part of a team, search for alive monsters within a range of 60 units and order them by distance + if (Body.Attributes[GameAttributes.Team_Override] == 1) + targets = Body.GetObjectsInRange(60f) + .Where(p => !p.Dead) + .OrderBy((monster) => PowerMath.Distance2D(monster.Position, Body.Position)) + .ToArray(); + else + // Otherwise, search for different types of actors including players, minions, destructible loot containers, or hirelings that are alive, not loading and not helpers, and order them by distance + targets = Body.GetActorsInRange(50f) + .Where(p => ((p is Player) && !p.Dead && p.Attributes[GameAttributes.Loading] == false && + p.Attributes[GameAttributes.Is_Helper] == false && + p.World.BuffManager.GetFirstBuff(p) == null) + || ((p is Minion) && !p.Dead && p.Attributes[GameAttributes.Is_Helper] == false) + || (p is DesctructibleLootContainer && p.SNO.IsDoorOrBarricade()) + || ((p is Hireling) && !p.Dead) + ) + .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) + .ToArray(); - if (powerToUse is 96925 or 223284) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f }; - } - else if (Body.WalkSpeed != 0) - { - if (Body.SNO.IsWoodwraithOrWasp()) - { - Logger.Trace($"{GetType().Name} $[underline white]${nameof(MoveToPointAction)}$[/]$ to target $[white]${_target.ActorType}$[/]$ [{_target.Position}]"); - CurrentAction = new MoveToPointAction( - Body, _target.Position - ); - } - else - { - Logger.Trace($"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}"); - CurrentAction = new MoveToTargetWithPathfindAction( - Body, - _target, - attackRange + _target.ActorData.Cylinder.Ax2, - powerToUse - ); - } - } - else - { - powerToUse = Body.SNO switch - { - ActorSno._a1dun_leor_firewall2 => 223284, - _ => powerToUse - }; - CurrentAction = new PowerAction(Body, powerToUse, _target); + // If there are no targets, return + if (targets.Length == 0) return; - if (power is SummoningSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) }; + // Set the first found target as the target + Target = targets.First(); + } + else + // If there is a priority target, set it as the target + Target = PriorityTarget; - if (power is MonsterAffixSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (power as MonsterAffixSkill).CooldownTime }; + int powerToUse = PickPowerToUse(); + if (powerToUse <= 0) return; + PowerScript power = PowerLoader.CreateImplementationForPowerSNO(powerToUse); + power.User = Body; + float attackRange = Body.ActorData.Cylinder.Ax2 + (power.EvalTag(PowerKeys.AttackRadius) > 0f + ? (powerToUse == 30592 ? 10f : Math.Min((float)power.EvalTag(PowerKeys.AttackRadius), 35f)) + : 35f); + float targetDistance = PowerMath.Distance2D(Target.Position, Body.Position); + if (targetDistance < attackRange + Target.ActorData.Cylinder.Ax2) + { + if (Body.WalkSpeed != 0) + Body.TranslateFacing(Target.Position, false); - if (PresetPowers[powerToUse].CooldownTime > 0f) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime }; + CurrentAction = new PowerAction(Body, powerToUse, Target); - if (powerToUse == 96925 || - powerToUse == 223284) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f }; - } - } - } - } + PresetPowers[powerToUse] = power switch + { + SummoningSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) + }, + MonsterAffixSkill monsterAffixSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime + }, + _ => PresetPowers[powerToUse] + }; - else if (Body.GetObjectsInRange(50f).Count != 0) - { - if (_powerDelay.TimedOut) - { - _powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f); + if (PresetPowers[powerToUse].CooldownTime > 0f) + PresetPowers[powerToUse] = new Cooldown + { + CooldownTimer = + new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), + CooldownTime = PresetPowers[powerToUse].CooldownTime + }; - if (AttackedBy != null) - PriorityTarget = AttackedBy; + if (powerToUse is 96925 or 223284) + PresetPowers[powerToUse] = new Cooldown + { CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f }; + } + else if (Body.WalkSpeed != 0) + { + if (Body.SNO.IsWoodwraithOrWasp()) + { + _logger.Trace( + $"{GetType().Name} $[underline white]${nameof(MoveToPointAction)}$[/]$ to target $[white]${Target.ActorType}$[/]$ [{Target.Position}]"); + CurrentAction = new MoveToPointAction( + Body, Target.Position + ); + } + else + { + _logger.Trace( + $"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}"); + CurrentAction = new MoveToTargetWithPathfindAction( + Body, + Target, + attackRange + Target.ActorData.Cylinder.Ax2, + powerToUse + ); + } + } + else + { + powerToUse = Body.SNO switch + { + ActorSno._a1dun_leor_firewall2 => 223284, + _ => powerToUse + }; + CurrentAction = new PowerAction(Body, powerToUse, Target); - if (PriorityTarget == null) - { - var targets = Body.GetActorsInRange(50f) - .Where(p => ((p is LorathNahr_NPC) && !p.Dead) - || ((p is CaptainRumford) && !p.Dead) - || (p is DesctructibleLootContainer && p.SNO.IsDoorOrBarricade()) - || ((p is Cain) && !p.Dead)) - .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) - .ToArray(); + PresetPowers[powerToUse] = power switch + { + SummoningSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) + }, + MonsterAffixSkill skill => new Cooldown + { + CooldownTimer = null, CooldownTime = skill.CooldownTime + }, + _ => PresetPowers[powerToUse] + }; - if (targets.Length == 0) - { - targets = Body.GetActorsInRange(20f) - .Where(p => ((p is Monster) && !p.Dead) - || ((p is CaptainRumford) && !p.Dead) - ) - .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) - .ToArray(); + if (PresetPowers[powerToUse].CooldownTime > 0f) + PresetPowers[powerToUse] = new Cooldown + { + CooldownTimer = + new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), + CooldownTime = PresetPowers[powerToUse].CooldownTime + }; + + if (powerToUse is 96925 or 223284) + PresetPowers[powerToUse] = new Cooldown + { CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f }; + } + } + + else if (Body.GetObjectsInRange(50f).Count != 0) + { + if (!_powerDelay.TimedOut) return; + _powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f); + + if (AttackedBy != null) + PriorityTarget = AttackedBy; + + if (PriorityTarget == null) + { + var targets = Body.GetActorsInRange(50f) + .Where(p => ((p is LorathNahr_NPC) && !p.Dead) + || ((p is CaptainRumford) && !p.Dead) + || (p is DesctructibleLootContainer && p.SNO.IsDoorOrBarricade()) + || ((p is Cain) && !p.Dead)) + .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) + .ToArray(); + + if (targets.Length == 0) + { + targets = Body.GetActorsInRange(20f) + .Where(p => ((p is Monster) && !p.Dead) + || ((p is CaptainRumford) && !p.Dead) + ) + .OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position)) + .ToArray(); - if (targets.Length == 0) - return; + if (targets.Length == 0) + return; - foreach (var monsterActor in targets.Where(tar => _target == null)) - if (monsterActor is Monster { Brain: MonsterBrain brain } monster && monsterActor != Body) - if (brain.AttackedBy != null) - _target = brain.AttackedBy; - } - else - { - _target = targets.First(); - } - foreach (var tar in targets) - if (tar is DesctructibleLootContainer && tar.SNO.IsDoorOrBarricade() && tar.SNO != ActorSno._trout_wagon_barricade) - { _target = tar; break; } - } - else - _target = PriorityTarget; + foreach (var monsterActor in targets.Where(tar => Target == null)) + if (monsterActor is Monster { Brain: MonsterBrain brain } monster && monsterActor != Body) + if (brain.AttackedBy != null) + Target = brain.AttackedBy; + } + else + { + Target = targets.First(); + } - int powerToUse = PickPowerToUse(); - if (powerToUse > 0) - { - PowerScript power = PowerLoader.CreateImplementationForPowerSNO(powerToUse); - power.User = Body; - if (_target == null) - { - /* - if (!this.Body.ActorSNO.Name.ToLower().Contains("woodwraith") && - !this.Body.ActorSNO.Name.ToLower().Contains("wasp")) - if (this.Body.Quality < 2) - { - this.CurrentAction = new MoveToPointWithPathfindAction(this.Body, RandomPosibleDirection(this.Body.CheckPointPosition, 3f, 8f, this.Body.World)); - return; - } - else - //*/ - return; - } - float attackRange = Body.ActorData.Cylinder.Ax2 + (power.EvalTag(PowerKeys.AttackRadius) > 0f ? (powerToUse == 30592 ? 10f : Math.Min((float)power.EvalTag(PowerKeys.AttackRadius), 35f)) : 35f); - float targetDistance = PowerMath.Distance2D(_target.Position, Body.Position); - if (targetDistance < attackRange + _target.ActorData.Cylinder.Ax2) - { - if (Body.WalkSpeed != 0) - Body.TranslateFacing(_target.Position, false); //columns and other non-walkable shit can't turn + foreach (var tar in targets) + if (tar is DesctructibleLootContainer && tar.SNO.IsDoorOrBarricade() && + tar.SNO != ActorSno._trout_wagon_barricade) + { + Target = tar; + break; + } + } + else + Target = PriorityTarget; - - Logger.Trace($"{GetType().Name} {nameof(PowerAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}"); - // Logger.Trace("PowerAction to target"); - CurrentAction = new PowerAction(Body, powerToUse, _target); + int powerToUse = PickPowerToUse(); + if (powerToUse > 0) + { + PowerScript power = PowerLoader.CreateImplementationForPowerSNO(powerToUse); + power.User = Body; + if (Target == null) + { + /* + if (!this.Body.ActorSNO.Name.ToLower().Contains("woodwraith") && + !this.Body.ActorSNO.Name.ToLower().Contains("wasp")) + if (this.Body.Quality < 2) + { + this.CurrentAction = new MoveToPointWithPathfindAction(this.Body, RandomPosibleDirection(this.Body.CheckPointPosition, 3f, 8f, this.Body.World)); + return; + } + else + //*/ + return; + } - if (power is SummoningSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) }; + float attackRange = Body.ActorData.Cylinder.Ax2 + (power.EvalTag(PowerKeys.AttackRadius) > 0f + ? (powerToUse == 30592 ? 10f : Math.Min((float)power.EvalTag(PowerKeys.AttackRadius), 35f)) + : 35f); + float targetDistance = PowerMath.Distance2D(Target.Position, Body.Position); + if (targetDistance < attackRange + Target.ActorData.Cylinder.Ax2) + { + if (Body.WalkSpeed != 0) + Body.TranslateFacing(Target.Position, + false); //columns and other non-walkable shit can't turn - if (power is MonsterAffixSkill monsterSkill) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = monsterSkill.CooldownTime }; - if (PresetPowers[powerToUse].CooldownTime > 0f) - PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime }; - } - else if (Body.WalkSpeed != 0) - { - if (Body.SNO.IsWoodwraithOrWasp()) - { - Logger.Trace($"{GetType().Name} {nameof(MoveToPointAction)} to target [{_target.Position}]"); + _logger.Trace( + $"{GetType().Name} {nameof(PowerAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}"); + // Logger.Trace("PowerAction to target"); + CurrentAction = new PowerAction(Body, powerToUse, Target); - CurrentAction = new MoveToPointAction( - Body, _target.Position - ); - } - else - { - Logger.Trace($"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}"); + PresetPowers[powerToUse] = power switch + { + SummoningSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) + }, + MonsterAffixSkill monsterSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = monsterSkill.CooldownTime + }, + _ => PresetPowers[powerToUse] + }; - CurrentAction = new MoveToTargetWithPathfindAction( - Body, - //( - _target,// + MovementHelpers.GetMovementPosition( - //new Vector3D(0, 0, 0), - //this.Body.WalkSpeed, - //MovementHelpers.GetFacingAngle(_target.Position, this.Body.Position), - //6 - //) - //) - attackRange + _target.ActorData.Cylinder.Ax2, - powerToUse - ); - } - } - } - } - } + if (PresetPowers[powerToUse].CooldownTime > 0f) + PresetPowers[powerToUse] = new Cooldown + { + CooldownTimer = new SecondsTickTimer(Body.World.Game, + PresetPowers[powerToUse].CooldownTime), + CooldownTime = PresetPowers[powerToUse].CooldownTime + }; + } + else if (Body.WalkSpeed != 0) + { + if (Body.SNO.IsWoodwraithOrWasp()) + { + _logger.Trace( + $"{GetType().Name} {nameof(MoveToPointAction)} to target [{Target.Position}]"); - else - { - //Logger.Trace("No enemies in range, return to master"); - if (Body.Position != Body.CheckPointPosition) - CurrentAction = new MoveToPointWithPathfindAction(Body, Body.CheckPointPosition); - } - } - } - public static Core.Types.Math.Vector3D RandomPossibleDirection(Core.Types.Math.Vector3D position, float minRadius, float maxRadius, MapSystem.World world) - { - float angle = (float)(FastRandom.Instance.NextDouble() * Math.PI * 2); - float radius = minRadius + (float)FastRandom.Instance.NextDouble() * (maxRadius - minRadius); - Core.Types.Math.Vector3D point = null; - int tryC = 0; - while (tryC < 100) - { - //break; - point = new Core.Types.Math.Vector3D(position.X + (float)Math.Cos(angle) * radius, - position.Y + (float)Math.Sin(angle) * radius, - position.Z); - if (world.CheckLocationForFlag(point, DiIiS_NA.Core.MPQ.FileFormats.Scene.NavCellFlags.AllowWalk)) - break; - tryC++; - } - return point; - } + CurrentAction = new MoveToPointAction( + Body, Target.Position + ); + } + else + { + _logger.Trace( + $"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}"); - public void FastAttack(Actor target, int skillSNO) - { - PowerScript power = PowerLoader.CreateImplementationForPowerSNO(skillSNO); - power.User = Body; - if (Body.WalkSpeed != 0) - Body.TranslateFacing(target.Position, false); //columns and other non-walkable shit can't turn - Logger.Trace($"{GetType().Name} {nameof(FastAttack)} {nameof(PowerAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}"); - CurrentAction = new PowerAction(Body, skillSNO, target); + CurrentAction = new MoveToTargetWithPathfindAction( + Body, + //( + Target, // + MovementHelpers.GetMovementPosition( + //new Vector3D(0, 0, 0), + //this.Body.WalkSpeed, + //MovementHelpers.GetFacingAngle(_target.Position, this.Body.Position), + //6 + //) + //) + attackRange + Target.ActorData.Cylinder.Ax2, + powerToUse + ); + } + } + } + } - if (power is SummoningSkill) - PresetPowers[skillSNO] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) }; + else + { + //Logger.Trace("No enemies in range, return to master"); + if (Body.Position != Body.CheckPointPosition) + CurrentAction = new MoveToPointWithPathfindAction(Body, Body.CheckPointPosition); + } + } - if (power is MonsterAffixSkill monsterAffixSkill) - PresetPowers[skillSNO] = new Cooldown { CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime }; + public static Core.Types.Math.Vector3D RandomPossibleDirection(Core.Types.Math.Vector3D position, + float minRadius, float maxRadius, MapSystem.World world) + { + float angle = (float)(FastRandom.Instance.NextDouble() * Math.PI * 2); + float radius = minRadius + (float)FastRandom.Instance.NextDouble() * (maxRadius - minRadius); + Core.Types.Math.Vector3D point = null; + int tryC = 0; + while (tryC < 100) + { + //break; + point = new Core.Types.Math.Vector3D(position.X + (float)Math.Cos(angle) * radius, + position.Y + (float)Math.Sin(angle) * radius, + position.Z); + if (world.CheckLocationForFlag(point, DiIiS_NA.Core.MPQ.FileFormats.Scene.NavCellFlags.AllowWalk)) + break; + tryC++; + } - if (PresetPowers[skillSNO].CooldownTime > 0f) - PresetPowers[skillSNO] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[skillSNO].CooldownTime), CooldownTime = PresetPowers[skillSNO].CooldownTime }; - } + return point; + } - protected virtual int PickPowerToUse() - { - if (!_warnedNoPowers && PresetPowers.Count == 0) - { - Logger.Warn($"Monster $[red]$\"{Body.Name}\"$[/]$ has no usable powers. {_mpqPowerCount} are defined in mpq data."); - _warnedNoPowers = true; - return -1; - } + public void FastAttack(Actor target, int skillSno) + { + PowerScript power = PowerLoader.CreateImplementationForPowerSNO(skillSno); + power.User = Body; + if (Body.WalkSpeed != 0) + Body.TranslateFacing(target.Position, false); //columns and other non-walkable shit can't turn + _logger.Trace( + $"{GetType().Name} {nameof(FastAttack)} {nameof(PowerAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}"); + CurrentAction = new PowerAction(Body, skillSno, target); - // randomly used an implemented power - if (PresetPowers.Count <= 0) return -1; - - //int power = this.PresetPowers[RandomHelper.Next(this.PresetPowers.Count)].Key; - var availablePowers = PresetPowers.Where(p => (p.Value.CooldownTimer == null || p.Value.CooldownTimer.TimedOut) && PowerLoader.HasImplementationForPowerSNO(p.Key)).Select(p => p.Key).ToList(); - if (availablePowers.Where(p => p != 30592).TryPickRandom(out var selectedPower)) - { - return selectedPower; - } + PresetPowers[skillSno] = power switch + { + SummoningSkill => new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) }, + MonsterAffixSkill monsterAffixSkill => new Cooldown + { + CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime + }, + _ => PresetPowers[skillSno] + }; - if (availablePowers.Contains(30592)) - return 30592; // melee attack + if (PresetPowers[skillSno].CooldownTime > 0f) + PresetPowers[skillSno] = new Cooldown + { + CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[skillSno].CooldownTime), + CooldownTime = PresetPowers[skillSno].CooldownTime + }; + } - // no usable power - return -1; - } + protected virtual int PickPowerToUse() + { + if (!_warnedNoPowers && PresetPowers.Count == 0) + { + _logger.Warn( + $"Monster $[red]$\"{Body.Name}\"$[/]$ has no usable powers. {_mpqPowerCount} are defined in mpq data."); + _warnedNoPowers = true; + return -1; + } - public void AddPresetPower(int powerSNO) - { - if (PresetPowers.ContainsKey(powerSNO)) - { - Logger.Debug($"Monster $[red]$\"{Body.Name}\"$[/]$ already has power {powerSNO}."); - // Logger.MethodTrace("power sno {0} already defined for monster \"{1}\"", - //powerSNO, this.Body.ActorSNO.Name); - return; - } + // randomly used an implemented power + if (PresetPowers.Count <= 0) return -1; - PresetPowers.Add(powerSNO, - PresetPowers.ContainsKey(30592) //if can cast melee - ? new Cooldown { CooldownTimer = null, CooldownTime = 5f } - : new Cooldown - { CooldownTimer = null, CooldownTime = 1f + (float)FastRandom.Instance.NextDouble() }); - } + //int power = this.PresetPowers[RandomHelper.Next(this.PresetPowers.Count)].Key; + var availablePowers = PresetPowers + .Where(p => (p.Value.CooldownTimer == null || p.Value.CooldownTimer.TimedOut) && + PowerLoader.HasImplementationForPowerSNO(p.Key)).Select(p => p.Key).ToList(); + if (availablePowers.Where(p => p != 30592).TryPickRandom(out var selectedPower)) + { + return selectedPower; + } - public void RemovePresetPower(int powerSNO) - { - if (PresetPowers.ContainsKey(powerSNO)) - { - PresetPowers.Remove(powerSNO); - } - } - } -} + if (availablePowers.Contains(30592)) + return 30592; // melee attack + + // no usable power + return -1; + } + + public void AddPresetPower(int powerSno) + { + if (PresetPowers.ContainsKey(powerSno)) + { + _logger.Debug($"Monster $[red]$\"{Body.Name}\"$[/]$ already has power {powerSno}."); + // Logger.MethodTrace("power sno {0} already defined for monster \"{1}\"", + //powerSNO, this.Body.ActorSNO.Name); + return; + } + + PresetPowers.Add(powerSno, + PresetPowers.ContainsKey(30592) //if can cast melee + ? new Cooldown { CooldownTimer = null, CooldownTime = 5f } + : new Cooldown + { CooldownTimer = null, CooldownTime = 1f + (float)FastRandom.Instance.NextDouble() }); + } + + public void RemovePresetPower(int powerSno) + { + if (PresetPowers.ContainsKey(powerSno)) + { + PresetPowers.Remove(powerSno); + } + } + } +} \ No newline at end of file diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Boss.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Boss.cs index 282a8f2..e5f26fc 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Boss.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Boss.cs @@ -4,6 +4,7 @@ using DiIiS_NA.GameServer.GSSystem.AISystem.Brains; using DiIiS_NA.GameServer.MessageSystem; using DiIiS_NA.Core.Logging; using DiIiS_NA.Core.MPQ.FileFormats; +using DiIiS_NA.D3_GameServer; namespace DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations { @@ -73,9 +74,9 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations //this.Attributes[GameAttribute.Immune_To_Charm] = true; Attributes[GameAttributes.using_Bossbar] = true; Attributes[GameAttributes.InBossEncounter] = true; - Attributes[GameAttributes.Hitpoints_Max] *= GameServerConfig.Instance.BossHealthMultiplier; - Attributes[GameAttributes.Damage_Weapon_Min, 0] *= GameServerConfig.Instance.BossDamageMultiplier; - Attributes[GameAttributes.Damage_Weapon_Delta, 0] *= GameServerConfig.Instance.BossDamageMultiplier; + Attributes[GameAttributes.Hitpoints_Max] *= GameModsConfig.Instance.Boss.HealthMultiplier; + Attributes[GameAttributes.Damage_Weapon_Min, 0] *= GameModsConfig.Instance.Boss.DamageMultiplier; + Attributes[GameAttributes.Damage_Weapon_Delta, 0] *= GameModsConfig.Instance.Boss.DamageMultiplier; Attributes[GameAttributes.Hitpoints_Cur] = Attributes[GameAttributes.Hitpoints_Max_Total]; Attributes[GameAttributes.TeamID] = 10; diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Monsters/Bosses.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Monsters/Bosses.cs index 465ef36..1ba12a8 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Monsters/Bosses.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Implementations/Monsters/Bosses.cs @@ -2,6 +2,7 @@ using DiIiS_NA.GameServer.Core.Types.TagMap; using DiIiS_NA.GameServer.GSSystem.MapSystem; using DiIiS_NA.GameServer.MessageSystem; +using Microsoft.Extensions.Logging; namespace DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations.Monsters { @@ -27,13 +28,9 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations.Monsters public override int Quality { - get - { - return (int)DiIiS_NA.Core.MPQ.FileFormats.SpawnType.Boss; - } + get => (int)DiIiS_NA.Core.MPQ.FileFormats.SpawnType.Boss; set { - } } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Monster.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Monster.cs index 6a1d9ad..a1f5336 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Monster.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Monster.cs @@ -6,6 +6,7 @@ using GameBalance = DiIiS_NA.Core.MPQ.FileFormats.GameBalance; using DiIiS_NA.GameServer.GSSystem.ObjectsSystem; using DiIiS_NA.Core.Logging; using DiIiS_NA.Core.MPQ.FileFormats; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.GameServer.GSSystem.TickerSystem; using DiIiS_NA.GameServer.MessageSystem; using DiIiS_NA.GameServer.Core.Types.SNO; @@ -22,7 +23,7 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem { public class Monster : Living, IUpdateable { - private static readonly Logger Logger = LogManager.CreateLogger(nameof(Monster)); + private static readonly Logger Logger = LogManager.CreateLogger(); public override ActorType ActorType => ActorType.Monster; public TickTimer DestroyTimer { get; } @@ -70,7 +71,7 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem //WalkSpeed /= 2f; Brain = new MonsterBrain(this); - Attributes[GameAttributes.Attacks_Per_Second] = 1.2f; + Attributes[GameAttributes.Attacks_Per_Second] = GameModsConfig.Instance.Monster.AttacksPerSecond;// 1.2f; UpdateStats(); } @@ -80,7 +81,7 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem #if DEBUG string monster = "monster"; if (this is Boss) monster = "boss"; - Logger.MethodTrace($"Player {player.Name} targeted {monster} {GetType().Name}."); + Logger.MethodTrace($"Player {player.Name} targeted $[underline]${monster}$[/]$ {GetType().Name}."); #endif } @@ -96,26 +97,26 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem Attributes[GameAttributes.Hitpoints_Max] = (int)((int)monsterLevels.MonsterLevel[monsterLevel].HPMin + DiIiS_NA.Core.Helpers.Math.RandomHelper.Next(0, (int)monsterLevels.MonsterLevel[monsterLevel].HPDelta) * HpMultiplier * World.Game.HpModifier); Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] = ((int)World.Game.ConnectedPlayers.Length + 1) * 1.5f; - Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] *= GameServerConfig.Instance.RateMonsterHP; + Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] *= GameModsConfig.Instance.Monster.HealthMultiplier; if (World.Game.ConnectedPlayers.Length > 1) Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] = Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative];// / 2f; var hpMax = Attributes[GameAttributes.Hitpoints_Max]; var hpTotal = Attributes[GameAttributes.Hitpoints_Max_Total]; float damageMin = monsterLevels.MonsterLevel[World.Game.MonsterLevel].Dmg * DmgMultiplier;// * 0.5f; float damageDelta = damageMin; - Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameServerConfig.Instance.RateMonsterDMG; + Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameModsConfig.Instance.Monster.DamageMultiplier; Attributes[GameAttributes.Damage_Weapon_Delta, 0] = damageDelta; if (monsterLevel > 30) { Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] = Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative];// * 0.5f; - Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameServerConfig.Instance.RateMonsterDMG;// * 0.2f; + Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameModsConfig.Instance.Monster.DamageMultiplier;// * 0.2f; Attributes[GameAttributes.Damage_Weapon_Delta, 0] = damageDelta; } if (monsterLevel > 60) { Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative] = Attributes[GameAttributes.Hitpoints_Max_Percent_Bonus_Multiplicative];// * 0.7f; - Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameServerConfig.Instance.RateMonsterDMG;// * 0.15f; + Attributes[GameAttributes.Damage_Weapon_Min, 0] = damageMin * World.Game.DmgModifier * GameModsConfig.Instance.Monster.DamageMultiplier;// * 0.15f; //this.Attributes[GameAttribute.Damage_Weapon_Delta, 0] = DamageDelta * 0.5f; } @@ -187,14 +188,12 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem lock (_adjustLock) { int count = player.World.Game.Players.Count; - if (count > 0 && _adjustedPlayers != count) - { - Attributes[GameAttributes.Damage_Weapon_Min, 0] = _nativeDmg * (1f + (0.05f * (count - 1) * player.World.Game.Difficulty)); - Attributes[GameAttributes.Hitpoints_Max] = _nativeHp * (1f + ((0.75f + (0.1f * player.World.Game.Difficulty)) * (count - 1))); - Attributes[GameAttributes.Hitpoints_Cur] = Attributes[GameAttributes.Hitpoints_Max_Total]; - Attributes.BroadcastChangedIfRevealed(); - _adjustedPlayers = count; - } + if (count <= 0 || _adjustedPlayers == count) return true; + Attributes[GameAttributes.Damage_Weapon_Min, 0] = _nativeDmg * (1f + (0.05f * (count - 1) * player.World.Game.Difficulty)); + Attributes[GameAttributes.Hitpoints_Max] = _nativeHp * (1f + ((0.75f + (0.1f * player.World.Game.Difficulty)) * (count - 1))); + Attributes[GameAttributes.Hitpoints_Cur] = Attributes[GameAttributes.Hitpoints_Max_Total]; + Attributes.BroadcastChangedIfRevealed(); + _adjustedPlayers = count; } return true; diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/ActEnum.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/ActEnum.cs new file mode 100644 index 0000000..adc99ec --- /dev/null +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/ActEnum.cs @@ -0,0 +1,11 @@ +namespace DiIiS_NA.GameServer.GSSystem.GameSystem; + +public enum ActEnum +{ + Act1 = 0, + Act2 = 100, + Act3 = 200, + Act4 = 300, + Act5 = 400, + OpenWorld = 3000 +} \ No newline at end of file diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/Game.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/Game.cs index f952a83..675beda 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/Game.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/Game.cs @@ -33,6 +33,7 @@ using DiIiS_NA.GameServer.GSSystem.AISystem.Brains; using System.Diagnostics; using System.Runtime.CompilerServices; using DiIiS_NA.Core.MPQ.FileFormats; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.D3_GameServer.Core.Types.SNO; using DiIiS_NA.D3_GameServer.GSSystem.GameSystem; using Actor = DiIiS_NA.GameServer.GSSystem.ActorSystem.Actor; @@ -42,16 +43,6 @@ using World = DiIiS_NA.GameServer.GSSystem.MapSystem.World; namespace DiIiS_NA.GameServer.GSSystem.GameSystem { - public enum ActEnum - { - Act1 = 0, - Act2 = 100, - Act3 = 200, - Act4 = 300, - Act5 = 400, - OpenWorld = 3000 - } - public class Game : IMessageConsumer { private static readonly Logger Logger = LogManager.CreateLogger(); @@ -66,6 +57,8 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem /// public ConcurrentDictionary Players { get; private set; } + public Player FirstPlayer() => Players.Values.First(); + public ImmutableArray ConnectedPlayers => Players .Where(s => s.Value != null && s.Key.Connection.IsOpen() && !s.Key.IsLoggingOut) .Select(s => s.Value).ToImmutableArray(); @@ -190,9 +183,10 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem /// Current quest SNOid. /// public int CurrentQuest = -1; - public int CurrentSideQuest = -1; + public bool IsCurrentOpenWorld => CurrentQuest == 312429; + /// /// Current quest step SNOid. /// @@ -1283,11 +1277,23 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem if (diff > 0) { var handicapLevels = (GameBalance)MPQStorage.Data.Assets[SNOGroup.GameBalance][256027].Data; - HpModifier = handicapLevels.HandicapLevelTables[diff].HPMod; - DmgModifier = handicapLevels.HandicapLevelTables[diff].DmgMod; - XpModifier = (1f + handicapLevels.HandicapLevelTables[diff].XPMod); - GoldModifier = (1f + handicapLevels.HandicapLevelTables[diff].GoldMod); + HpModifier = handicapLevels.HandicapLevelTables[diff].HPMod * GameModsConfig.Instance.Rate.HealthByDifficulty[Difficulty] + * GameModsConfig.Instance.Monster.HealthMultiplier; + DmgModifier = handicapLevels.HandicapLevelTables[diff].DmgMod + * GameModsConfig.Instance.Rate.GetDamageByDifficulty(diff) + * GameModsConfig.Instance.Monster.DamageMultiplier; + XpModifier = (1f + handicapLevels.HandicapLevelTables[diff].XPMod) * GameModsConfig.Instance.Rate.Experience; + GoldModifier = (1f + handicapLevels.HandicapLevelTables[diff].GoldMod * GameModsConfig.Instance.Rate.Gold); } + else + { + HpModifier = GameModsConfig.Instance.Rate.HealthByDifficulty[diff] * GameModsConfig.Instance.Monster.HealthMultiplier; + DmgModifier = GameModsConfig.Instance.Rate.GetDamageByDifficulty(diff) * GameModsConfig.Instance.Monster.DamageMultiplier; + XpModifier = 1f + GameModsConfig.Instance.Rate.Experience; + GoldModifier = (1f * GameModsConfig.Instance.Rate.Gold); + } + + Logger.Info($"$[italic]$Updated Game #$[underline]${GameId}$[/]$ difficulty to {diff}.$[/]$"); foreach (var wld in _worlds) foreach (var monster in wld.Value.Monsters) @@ -1570,9 +1576,8 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem //handling quest triggers - if (QuestProgress.QuestTriggers.ContainsKey(levelArea)) //EnterLevelArea + if (QuestProgress.QuestTriggers.TryGetValue(levelArea, out var trigger)) //EnterLevelArea { - var trigger = QuestProgress.QuestTriggers[levelArea]; if (trigger.TriggerType == QuestStepObjectiveType.EnterLevelArea) { try @@ -1645,7 +1650,7 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem //Берем каина var firstPoint = new Vector3D(120.92718f, 121.26151f, 0.099973306f); var secondPoint = new Vector3D(120.73298f, 160.61829f, 0.31863004f); - var sceletonPoint = new Vector3D(120.11514f, 140.77332f, 0.31863004f); + var sketonPosition = new Vector3D(120.11514f, 140.77332f, 0.31863004f); var firstfacingAngle = ActorSystem.Movement.MovementHelpers.GetFacingAngle(cainRun, firstPoint); @@ -1666,9 +1671,9 @@ namespace DiIiS_NA.GameServer.GSSystem.GameSystem { foreach (var skeleton in skeletons) { - skeleton.Move(sceletonPoint, + skeleton.Move(sketonPosition, ActorSystem.Movement.MovementHelpers.GetFacingAngle(skeleton, - sceletonPoint)); + sketonPosition)); } cainRun.Move(secondPoint, secondfacingAngle); diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/QuestManager.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/QuestManager.cs index ea1ac37..66b34fa 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/QuestManager.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/GameSystem/QuestManager.cs @@ -21,6 +21,7 @@ using DiIiS_NA.GameServer.MessageSystem; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Map; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Quest; using DiIiS_NA.GameServer.MessageSystem.Message.Fields; +using Spectre.Console; using Monster = DiIiS_NA.GameServer.GSSystem.ActorSystem.Monster; namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem @@ -29,11 +30,6 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem { private static readonly Logger Logger = new(nameof(QuestManager)); - /// - /// Accessor for quests - /// - /// snoId of the quest to retrieve - /// public readonly Dictionary Quests = new(); public readonly Dictionary SideQuests = new(); @@ -164,12 +160,30 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem Bounties.AddRange(actToKillUniqueBounties[BountyData.ActT.A5].Take(4)); } + private readonly struct Rewards + { + public int Experience { get; } + public int Gold { get; } + + public Rewards(int experience, int gold) + { + Experience = experience; + Gold = gold; + } + + public Rewards(float experience, float gold) : this((int) Math.Floor(experience), (int) Math.Floor(gold)) {} + } + + private Rewards GetCurrentQuestRewards() => + new Rewards(Quests[Game.CurrentQuest].RewardXp, Quests[Game.CurrentQuest].RewardGold); /// /// Advances a quest by a step /// /// snoID of the quest to advance public void Advance() { + int oldQuest = Game.CurrentQuest; + int oldStep = Game.CurrentStep; Quests[Game.CurrentQuest].Steps[Game.CurrentStep].Completed = true; Game.CurrentStep = Quests[Game.CurrentQuest].Steps[Game.CurrentStep].NextStep; Game.QuestProgress.QuestTriggers.Clear(); @@ -185,6 +199,13 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem if (Quests[Game.CurrentQuest].Steps[Game.CurrentStep].NextStep != -1) { + Logger.QuestInfo( + $"{Emoji.Known.RightArrow} Step Advance ".StyleAnsi("deeppink4") + + $"Game #{Game.GameId.StyleAnsi("underline")} " + + $"from quest {oldQuest}/" + + $"step {oldStep.StyleAnsi("deeppink4")}" + + $"to quest {Game.CurrentQuest}'s " + + $"step {Game.CurrentStep.StyleAnsi("deeppink4")}"); } else { @@ -192,23 +213,25 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem if (!Game.Empty) { SaveQuestProgress(true); - Logger.Trace( - $"$[white]$(Advance)$[/]$ Game {Game.GameId} Advanced to quest $[underline white]${Game.CurrentQuest}$[/]$, completed $[underline white]${Quests[Game.CurrentQuest].Completed}$[/]$"); + Logger.QuestInfo( + $"{Emoji.Known.NextTrackButton} Quest Advance ".StyleAnsi("white") + + $"Game #{Game.GameId.StyleAnsi("underline")} " + + $"from quest {oldQuest.StyleAnsi("turquoise2")}/" + + $"step {oldStep.StyleAnsi("deeppink4")}" + + $"to quest {Game.CurrentQuest.StyleAnsi("turquoise2")}/" + + $"step {Game.CurrentStep.StyleAnsi("deeppink4")}"); Game.BroadcastPlayers((client, player) => { - if (Game.CurrentQuest == 312429) return; // open world quest + if (Game.IsCurrentOpenWorld) return; // open world quest - int xpReward = (int)(Quests[Game.CurrentQuest].RewardXp * - Game.XpModifier); - int goldReward = (int)(Quests[Game.CurrentQuest].RewardGold * - Game.GoldModifier); + var rewards = GetCurrentQuestRewards(); player.InGameClient.SendMessage(new QuestStepCompleteMessage() { QuestStepComplete = QuestStepComplete.CreateBuilder() .SetReward(QuestReward.CreateBuilder() - .SetGoldGranted(goldReward) - .SetXpGranted((ulong)xpReward) + .SetGoldGranted(rewards.Gold) + .SetXpGranted((ulong)rewards.Experience) .SetSnoQuest(Game.CurrentQuest) ) .SetIsQuestComplete(true) @@ -224,7 +247,7 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem WorldID = player.World.DynamicID(player), }, - Amount = xpReward, + Amount = rewards.Experience, Type = GameServer.MessageSystem.Message.Definitions.Base .FloatingAmountMessage.FloatType.Experience, }); @@ -238,13 +261,13 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem WorldID = player.World.DynamicID(player), }, - Amount = goldReward, + Amount = rewards.Gold, Type = GameServer.MessageSystem.Message.Definitions.Base .FloatingAmountMessage.FloatType.Gold, }); - player.UpdateExp(xpReward); - player.Inventory.AddGoldAmount(goldReward); - player.AddAchievementCounter(74987243307173, (uint)goldReward); + player.UpdateExp(rewards.Experience); + player.Inventory.AddGoldAmount(rewards.Gold); + player.AddAchievementCounter(74987243307173, (uint)rewards.Gold); player.CheckQuestCriteria(Game.CurrentQuest); }); } @@ -270,7 +293,7 @@ namespace DiIiS_NA.D3_GameServer.GSSystem.GameSystem if (!Game.Empty) { RevealQuestProgress(); - if ((Game.CurrentActEnum != ActEnum.OpenWorld && GameServerConfig.Instance.AutoSaveQuests) || + if ((Game.CurrentActEnum != ActEnum.OpenWorld && GameModsConfig.Instance.Quest.AutoSave) || Quests[Game.CurrentQuest].Steps[Game.CurrentStep].Saveable) SaveQuestProgress(false); } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/ItemGenerator.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/ItemGenerator.cs index 81fabf9..7894bee 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/ItemGenerator.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/ItemGenerator.cs @@ -18,6 +18,7 @@ using DiIiS_NA.GameServer.Core.Types.TagMap; using DiIiS_NA.GameServer.MessageSystem; using DiIiS_NA.LoginServer.Toons; using DiIiS_NA.Core.Helpers.Math; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.GameServer.GSSystem.PlayerSystem; namespace DiIiS_NA.GameServer.GSSystem.ItemsSystem @@ -1358,8 +1359,8 @@ namespace DiIiS_NA.GameServer.GSSystem.ItemsSystem private static void RandomSetUnidentified(Item item) => item.Unidentified = FastRandom.Instance.Chance(item.Name.Contains("unique", StringComparison.InvariantCultureIgnoreCase) || item.ItemDefinition.Quality is ItemTable.ItemQuality.Legendary or ItemTable.ItemQuality.Special or ItemTable.ItemQuality.Set - ? GameServerConfig.Instance.ChanceHighQualityUnidentified - : GameServerConfig.Instance.ChanceNormalUnidentified); + ? GameModsConfig.Instance.Items.UnidentifiedDropChances.HighQuality + : GameModsConfig.Instance.Items.UnidentifiedDropChances.NormalQuality); // Allows cooking a custom item. public static Item Cook(Player player, string name) diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/LootManager.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/LootManager.cs index 30ddf39..5d85f2e 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/LootManager.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/ItemsSystem/LootManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using DiIiS_NA.D3_GameServer; namespace DiIiS_NA.GameServer.GSSystem.ItemsSystem { @@ -613,16 +614,16 @@ namespace DiIiS_NA.GameServer.GSSystem.ItemsSystem switch (MonsterQuality) { case 0: //Normal - return new List { 0.18f * GameServerConfig.Instance.RateChangeDrop }; + return new List { 0.18f * GameModsConfig.Instance.Rate.ChangeDrop }; case 1: //Champion - return new List { 1f, 1f, 1f, 1f, 0.75f * GameServerConfig.Instance.RateChangeDrop }; + return new List { 1f, 1f, 1f, 1f, 0.75f * GameModsConfig.Instance.Rate.ChangeDrop }; case 2: //Rare (Elite) case 4: //Unique return new List { 1f, 1f, 1f, 1f, 1f }; case 7: //Boss - return new List { 1f, 1f, 1f, 1f, 1f, 0.75f * GameServerConfig.Instance.RateChangeDrop, 0.4f * GameServerConfig.Instance.RateChangeDrop }; + return new List { 1f, 1f, 1f, 1f, 1f, 0.75f * GameModsConfig.Instance.Rate.ChangeDrop, 0.4f * GameModsConfig.Instance.Rate.ChangeDrop }; default: - return new List { 0.12f * GameServerConfig.Instance.RateChangeDrop }; + return new List { 0.12f * GameModsConfig.Instance.Rate.ChangeDrop }; } } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/Scene.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/Scene.cs index 5c5352f..8c23019 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/Scene.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/Scene.cs @@ -24,6 +24,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using DiIiS_NA.D3_GameServer; using Actor = DiIiS_NA.GameServer.GSSystem.ActorSystem.Actor; namespace DiIiS_NA.GameServer.GSSystem.MapSystem @@ -549,7 +550,7 @@ namespace DiIiS_NA.GameServer.GSSystem.MapSystem SceneSNO = SceneSNO.Id, Transform = Transform, WorldID = World.GlobalID, - MiniMapVisibility = GameServerConfig.Instance.ForceMinimapVisibility + MiniMapVisibility = GameModsConfig.Instance.Minimap.ForceVisibility }; } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/World.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/World.cs index 168bbc8..aa01ea6 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/World.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/MapSystem/World.cs @@ -10,6 +10,7 @@ using DiIiS_NA.Core.Helpers.Math; using DiIiS_NA.Core.Logging; using DiIiS_NA.Core.MPQ; using DiIiS_NA.Core.MPQ.FileFormats; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.D3_GameServer.Core.Types.SNO; using DiIiS_NA.GameServer.Core.Types.Math; using DiIiS_NA.GameServer.Core.Types.QuadTrees; @@ -930,7 +931,7 @@ namespace DiIiS_NA.GameServer.GSSystem.MapSystem /// The position for drop. public void SpawnGold(Actor source, Player player, int Min = -1) { - int amount = (int)(LootManager.GetGoldAmount(player.Attributes[GameAttributes.Level]) * Game.GoldModifier * GameServerConfig.Instance.RateMoney); + int amount = (int)(LootManager.GetGoldAmount(player.Attributes[GameAttributes.Level]) * Game.GoldModifier * GameModsConfig.Instance.Rate.Gold); if (Min != -1) amount += Min; var item = ItemGenerator.CreateGold(player, amount); // somehow the actual ammount is not shown on ground /raist. diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PlayerSystem/Player.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PlayerSystem/Player.cs index e213ba8..238ed58 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PlayerSystem/Player.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PlayerSystem/Player.cs @@ -56,10 +56,12 @@ using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Pet; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Game; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Hireling; using DiIiS_NA.Core.Helpers.Hash; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Encounter; using DiIiS_NA.D3_GameServer.Core.Types.SNO; using DiIiS_NA.D3_GameServer.GSSystem.ActorSystem.Implementations.Artisans; using DiIiS_NA.D3_GameServer.GSSystem.PlayerSystem; +using DiIiS_NA.LoginServer; using NHibernate.Util; namespace DiIiS_NA.GameServer.GSSystem.PlayerSystem; @@ -2480,6 +2482,21 @@ public class Player : Actor, IMessageConsumer, IUpdateable public bool SpeedCheckDisabled = false; + public float StrengthMultiplier => ParagonLevel > 0 + ? GameModsConfig.Instance.Player.Multipliers.Strength.Paragon + : GameModsConfig.Instance.Player.Multipliers.Strength.Normal; + public float DexterityMultiplier => ParagonLevel > 0 + ? GameModsConfig.Instance.Player.Multipliers.Dexterity.Paragon + : GameModsConfig.Instance.Player.Multipliers.Dexterity.Normal; + + public float IntelligenceMultiplier => ParagonLevel > 0 + ? GameModsConfig.Instance.Player.Multipliers.Intelligence.Paragon + : GameModsConfig.Instance.Player.Multipliers.Intelligence.Normal; + + public float VitalityMultiplier => ParagonLevel > 0 + ? GameModsConfig.Instance.Player.Multipliers.Vitality.Paragon + : GameModsConfig.Instance.Player.Multipliers.Intelligence.Normal; + public static byte[] StringToByteArray(string hex) { return Enumerable.Range(0, hex.Length) @@ -2674,8 +2691,9 @@ public class Player : Actor, IMessageConsumer, IUpdateable Logger.WarnException(e, "questEvent()"); } } - // Reset resurrection charges on zone change - TODO: do not reset charges on reentering the same zone - Attributes[GameAttributes.Corpse_Resurrection_Charges] = GameServerConfig.Instance.ResurrectionCharges; + // Reset resurrection charges on zone change + // TODO: do not reset charges on reentering the same zone + Attributes[GameAttributes.Corpse_Resurrection_Charges] = GameModsConfig.Instance.Health.ResurrectionCharges; #if DEBUG Logger.Warn($"Player Location {Toon.Name}, Scene: {CurrentScene.SceneSNO.Name} SNO: {CurrentScene.SceneSNO.Id} LevelArea: {CurrentScene.Specification.SNOLevelAreas[0]}"); @@ -3607,7 +3625,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable if (!_motdSent && LoginServer.LoginServerConfig.Instance.MotdEnabled) { - if (GameServerConfig.Instance.MotdWhenWorldLoads) + if (!LoginServerConfig.Instance.MotdEnabledWhenWorldLoads) _motdSent = true; InGameClient.BnetClient.SendMotd(); } @@ -3994,8 +4012,8 @@ public class Player : Actor, IMessageConsumer, IUpdateable get { var baseStrength = 0.0f; - var multiplier = ParagonLevel > 0 ? GameServerConfig.Instance.StrengthParagonMultiplier : GameServerConfig.Instance.StrengthMultiplier; - baseStrength = Toon.HeroTable.CoreAttribute == GameBalance.PrimaryAttribute.Strength + var multiplier = StrengthMultiplier; + baseStrength = Toon.HeroTable.CoreAttribute == GameBalance.PrimaryAttribute.Strength ? Toon.HeroTable.Strength + (Level - 1) * 3 : Toon.HeroTable.Strength + (Level - 1); @@ -4010,8 +4028,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable { get { - var multiplier = ParagonLevel > 0 ? GameServerConfig.Instance.DexterityParagonMultiplier : GameServerConfig.Instance.DexterityMultiplier; - + var multiplier = DexterityMultiplier; return Toon.HeroTable.CoreAttribute == GameBalance.PrimaryAttribute.Dexterity ? Toon.HeroTable.Dexterity + (Level - 1) * 3 * multiplier : Toon.HeroTable.Dexterity + (Level - 1) * multiplier; @@ -4021,7 +4038,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable public float TotalDexterity => Attributes[GameAttributes.Dexterity] + Inventory.GetItemBonus(GameAttributes.Dexterity_Item); - public float Vitality => Toon.HeroTable.Vitality + (Level - 1) * 2 * (ParagonLevel > 0 ? GameServerConfig.Instance.VitalityParagonMultiplier : GameServerConfig.Instance.VitalityMultiplier); + public float Vitality => Toon.HeroTable.Vitality + (Level - 1) * 2 * (VitalityMultiplier); public float TotalVitality => Attributes[GameAttributes.Vitality] + Inventory.GetItemBonus(GameAttributes.Vitality_Item); @@ -4030,7 +4047,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable { get { - var multiplier = ParagonLevel > 0 ? GameServerConfig.Instance.IntelligenceParagonMultiplier : GameServerConfig.Instance.IntelligenceMultiplier; + var multiplier = IntelligenceMultiplier; return Toon.HeroTable.CoreAttribute == GameBalance.PrimaryAttribute.Intelligence ? Toon.HeroTable.Intelligence + (Level - 1) * 3 * multiplier : Toon.HeroTable.Intelligence + (Level - 1) * multiplier; @@ -4089,7 +4106,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable }, SkillSlotEverAssigned = 0x0F, //0xB4, PlaytimeTotal = Toon.TimePlayed, - WaypointFlags = GameServerConfig.Instance.UnlockAllWaypoints ? 0x0000ffff : World.Game.WaypointFlags, + WaypointFlags = GameModsConfig.Instance.Quest.UnlockAllWaypoints ? 0x0000ffff : World.Game.WaypointFlags, HirelingData = new HirelingSavedData() { HirelingInfos = HirelingInfo, @@ -5498,7 +5515,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable { if (InGameClient.Game.ActiveNephalemTimer && InGameClient.Game.ActiveNephalemKilledMobs == false) { - InGameClient.Game.ActiveNephalemProgress += 15f * GameServerConfig.Instance.NephalemRiftProgressMultiplier; + InGameClient.Game.ActiveNephalemProgress += 15f * GameModsConfig.Instance.NephalemRift.ProgressMultiplier; foreach (var plr in InGameClient.Game.Players.Values) { plr.InGameClient.SendMessage(new FloatDataMessage(Opcodes.DunggeonFinderProgressGlyphPickUp) diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/General/DrinkHealthPotion.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/General/DrinkHealthPotion.cs index 28b7709..8877e1e 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/General/DrinkHealthPotion.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/General/DrinkHealthPotion.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.GameServer.GSSystem.PlayerSystem; using DiIiS_NA.GameServer.GSSystem.TickerSystem; using DiIiS_NA.LoginServer; @@ -12,8 +13,8 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations.General public override IEnumerable Run() { if (User is not Player player) yield break; - player.AddPercentageHP(GameServerConfig.Instance.HealthPotionRestorePercentage); - AddBuff(player, player, new CooldownBuff(30211, TickTimer.WaitSeconds(player.World.Game, GameServerConfig.Instance.HealthPotionCooldown))); + player.AddPercentageHP(GameModsConfig.Instance.Health.PotionRestorePercentage); + AddBuff(player, player, new CooldownBuff(30211, TickTimer.WaitSeconds(player.World.Game, GameModsConfig.Instance.Health.PotionCooldown))); } } } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Crusader.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Crusader.cs index 5de07be..6f00fd8 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Crusader.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Crusader.cs @@ -148,7 +148,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations if (Rune_B > 0) DmgType = DamageType.Lightning; //Electrify AttackPayload attack = new AttackPayload(this); attack.Targets = GetEnemiesInArcDirection(User.Position, TargetPosition, 12f, Rune_D > 0 ? 120f : 90f); //Carve - if (Rune_C > 0) attack.chcBonus = ScriptFormula(14); //Crush + if (Rune_C > 0) attack.ChcBonus = ScriptFormula(14); //Crush attack.AddWeaponDamage(ScriptFormula(0), DmgType); attack.OnHit = hitPayload => { @@ -3150,7 +3150,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations attack.Targets = GetEnemiesInRadius(point, 12f); attack.AddWeaponDamage(ScriptFormula(3), DamageType.Physical); if (Rune_B > 0) //Annihilate - attack.chcBonus = 1f; //will be capped to 85% anyway + attack.ChcBonus = 1f; //will be capped to 85% anyway attack.OnHit = (hitPayload) => { if (Rune_A > 0) //Barrels of tar diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Necromancer.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Necromancer.cs index 7628543..94630fa 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Necromancer.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Implementations/HeroSkills/Necromancer.cs @@ -1264,7 +1264,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations } } #endregion - //Done + //Done - testing, apparently Rune_A not working. #region CorpseExlosion [ImplementsPowerSNO(SkillsSystem.Skills.Necromancer.ExtraSkills.CorpseExlosion)] @@ -1272,72 +1272,86 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations { public override IEnumerable Main() { - //ScriptFormulaDetails_Fields - //PowerDefinition_Fields - //Мертвячинка) - if (player.SkillSet.HasPassive(208594)) 454066 - if (Rune_B > 0) - ((Player) User).AddPercentageHP(-2); - float Radius = 20f; - float Damage = 10.5f; - DamageType DType = DamageType.Physical; - var PowerData = (DiIiS_NA.Core.MPQ.FileFormats.Power)MPQStorage.Data.Assets[SNOGroup.Power][PowerSNO].Data; - var Point = SpawnEffect(ActorSno._p6_necro_bonespikes, TargetPosition, 0, WaitSeconds(0.2f)); - Point.PlayEffect(Effect.PlayEffectGroup, RuneSelect(459954, 473926, 459954, 473907, 459954//D - , 473864)); + // Initializing main variables for Bonespikes ability. + float radius = 20f; + float damage = 10.5f; + DamageType damageType = DamageType.Physical; + + // Fetching the data for the respective Power from the MPQ cache. + var powerData = (DiIiS_NA.Core.MPQ.FileFormats.Power)MPQStorage.Data.Assets[SNOGroup.Power][PowerSNO].Data; + + // Creating a point effect on the target position, playing various effect groups depending on the selected Rune. + var point = SpawnEffect(ActorSno._p6_necro_bonespikes, TargetPosition, 0, WaitSeconds(0.2f)); + point.PlayEffect(Effect.PlayEffectGroup, RuneSelect(459954, 473926, 459954, 473907, 459954, 473864)); + + // Depending on a specific game attribute, either spawn a new monster at the target position, or select up to five existing corpses. var actors = User.Attributes[GameAttributes.Necromancer_Corpse_Free_Casting] ? new List { User.World.SpawnMonster(ActorSno._p6_necro_corpse_flesh, TargetPosition).GlobalID } - : User.GetActorsInRange(TargetPosition, 11).Where(x => x.SNO == ActorSno._p6_necro_corpse_flesh).Select(x => x.GlobalID).Take(5).ToList(); - if (Rune_D > 0) - Radius = 25f; - else if (Rune_C > 0)//licking action - { Damage = 15.75f; DType = DamageType.Poison; } - else if (Rune_A > 0) - DType = DamageType.Poison; + : User.GetActorsInRange(TargetPosition, 11).Where(x => x.SNO == ActorSno._p6_necro_corpse_flesh) + .Select(x => x.GlobalID).Take(5).ToList(); + // Modifying main parameters of the ability depending on the selected Rune. + if (Rune_D > 0) + { + radius = 25f; + } + else if (Rune_C > 0) // Licking action. + { + damage = 15.75f; + damageType = DamageType.Poison; + } + else if (Rune_A > 0) + { + damageType = DamageType.Poison; + } + + // Applying the effects of the Bonespikes ability on the selected corpses. foreach (var actor in actors) { - if (Rune_B > 0) { var bomb = World.GetActorByGlobalId(actor); var nearestEnemy = bomb.GetActorsInRange(20f).First(); if (nearestEnemy != null) bomb.Teleport(nearestEnemy.Position); - } - - var Explosion = SpawnEffect( + + // Spawning explosion effect. + var explosionEffect = SpawnEffect( ActorSno._p6_necro_corpseexplosion_projectile_spawn, World.GetActorByGlobalId(actor).Position, ActorSystem.Movement.MovementHelpers.GetFacingAngle(User, World.GetActorByGlobalId(actor)), WaitSeconds(0.2f) ); - Explosion.PlayEffect(Effect.PlayEffectGroup, RuneSelect(457183, 471539, 471258, 471249, 471247, 471236)); + explosionEffect.PlayEffect(Effect.PlayEffectGroup, + RuneSelect(457183, 471539, 471258, 471249, 471247, 471236)); + explosionEffect.UpdateDelay = 0.1f; - Explosion.UpdateDelay = 0.1f; - Explosion.OnUpdate = () => + explosionEffect.OnUpdate = () => { - AttackPayload attack = new AttackPayload(this) + // Creating the attack payload. + AttackPayload attack = new(this) { - Targets = GetEnemiesInRadius(User.Position, Radius) + Targets = GetEnemiesInRadius(User.Position, radius) }; if (Rune_E > 0) - DType = DamageType.Cold; - - attack.AddWeaponDamage(Damage, DType); + damageType = DamageType.Cold; + + // Applying weapon damage. + attack.AddWeaponDamage(damage, damageType); attack.OnHit = hitPayload => { if (Rune_E > 0) AddBuff(hitPayload.Target, new DebuffFrozen(WaitSeconds(2f))); }; + // Applying the attack. attack.Apply(); }; + // Destroying the selected corpse. World.GetActorByGlobalId(actor).Destroy(); } - - //}); yield break; } } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/AttackPayload.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/AttackPayload.cs index d4a1ced..12dc3a5 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/AttackPayload.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/AttackPayload.cs @@ -16,7 +16,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads // list of targets to try and hit with this payload, must be set before calling Apply() public TargetList Targets; - public float chcBonus = 0f; + public float ChcBonus = 0f; // list of each amount and type of damage the attack will contain public class DamageEntry @@ -115,7 +115,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads if (target == null || target.World == null || target.World != null && target.World.PowerManager.IsDeletingActor(target)) continue; - var payload = new HitPayload(this, _DoCriticalHit(Context.User, target, chcBonus) + var payload = new HitPayload(this, _DoCriticalHit(Context.User, target, ChcBonus) , target); payload.AutomaticHitEffects = AutomaticHitEffects; payload.OnDeath = OnDeath; diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/DeathPayload.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/DeathPayload.cs index 720170d..33cb72f 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/DeathPayload.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/PowerSystem/Payloads/DeathPayload.cs @@ -14,6 +14,7 @@ using DiIiS_NA.GameServer.GSSystem.PowerSystem.Implementations; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Effect; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Combat; using DiIiS_NA.Core.Helpers.Math; +using DiIiS_NA.D3_GameServer; using DiIiS_NA.LoginServer.Toons; using DiIiS_NA.GameServer.Core.Types.TagMap; using DiIiS_NA.GameServer.GSSystem.GeneratorsSystem; @@ -425,7 +426,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads { grantedExp = (int)(grantedExp * rangedPlayer.World.Game.XpModifier); - float tempExp = grantedExp * GameServerConfig.Instance.RateExp; + float tempExp = grantedExp * GameModsConfig.Instance.Rate.Experience; rangedPlayer.UpdateExp(Math.Max((int)tempExp, 1)); var a = (int)rangedPlayer.Attributes[GameAttributes.Experience_Bonus]; @@ -636,13 +637,13 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads Target.World.Game.ActiveNephalemTimer && Target.World.Game.ActiveNephalemKilledMobs == false) { Target.World.Game.ActiveNephalemProgress += - GameServerConfig.Instance.NephalemRiftProgressMultiplier * (Target.Quality + 1); + GameModsConfig.Instance.NephalemRift.ProgressMultiplier * (Target.Quality + 1); Player master = null; foreach (var plr3 in Target.World.Game.Players.Values) { if (plr3.PlayerIndex == 0) master = plr3; - if (GameServerConfig.Instance.NephalemRiftAutoFinish && Target.World.Monsters.Count(s => !s.Dead) <= GameServerConfig.Instance.NephalemRiftAutoFinishThreshold) Target.World.Game.ActiveNephalemProgress = 651; + if (GameModsConfig.Instance.NephalemRift.AutoFinish && Target.World.Monsters.Count(s => !s.Dead) <= GameModsConfig.Instance.NephalemRift.AutoFinishThreshold) Target.World.Game.ActiveNephalemProgress = 651; plr3.InGameClient.SendMessage(new SimpleMessage(Opcodes.KillCounterRefresh)); plr3.InGameClient.SendMessage(new FloatDataMessage(Opcodes.DungeonFinderProgressMessage) { @@ -711,7 +712,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads } - if (Target.Quality > 1 || FastRandom.Instance.Chance(GameServerConfig.Instance.NephalemRiftOrbsChance)) + if (Target.Quality > 1 || FastRandom.Instance.Chance(GameModsConfig.Instance.NephalemRift.OrbsChance)) { //spawn spheres for mining indicator for (int i = 0; i < Target.Quality + 1; i++) @@ -938,7 +939,7 @@ namespace DiIiS_NA.GameServer.GSSystem.PowerSystem.Payloads // if seed is less than the drop rate, drop the item if (seed < rate * (1f + lootSpawnPlayer.Attributes[GameAttributes.Magic_Find]) - * GameServerConfig.Instance.RateDrop) + * GameModsConfig.Instance.Rate.Drop) { //Logger.Debug("rate: {0}", rate); var lootQuality = Target.World.Game.IsHardcore diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/ActI.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/ActI.cs index 8b55eca..109c8c4 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/ActI.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/ActI.cs @@ -244,16 +244,23 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem //AddFollower(this.Game.GetWorld(71150), 4580); Game.AddOnLoadWorldAction(WorldSno.trout_town, () => { - // TODO: CHeck for possible removing outer adding - Game.AddOnLoadWorldAction(WorldSno.trout_town, () => + if (Game.CurrentQuest == 72095 && Game.CurrentStep is -1 or 7) { - if (Game.CurrentQuest == 72095) - if (Game.CurrentStep == -1 || Game.CurrentStep == 7) - { - AddFollower(Game.GetWorld(WorldSno.trout_town), ActorSno._leah); - } - }); - + // var world = Game.GetWorld(WorldSno.trout_town); + // Logger.QuestStep("Adding leah follower"); + // // teleport leah + // var actor = world.GetActorBySNO(ActorSno._leah); + // if (actor != null) + // { + // actor.Teleport(Game.FirstPlayer().Position.Around(2f)); + // } + AddUniqueFollower(Game.GetWorld(WorldSno.trout_town), ActorSno._leah); + } + else + { + Logger.QuestStep($"Can't add leah follower: {Game.CurrentQuest} / {Game.CurrentStep}"); + } + }); } }); @@ -265,6 +272,7 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem NextStep = 49, OnAdvance = () => { //go to gates + AddUniqueFollower(Game.GetWorld(WorldSno.trout_town), ActorSno._leah); var world = Game.GetWorld(WorldSno.trout_town); StartConversation(world, 166678); ListenProximity(ActorSno._trout_oldtristram_exit_gate, new Advance()); @@ -409,12 +417,16 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem Saveable = true, NextStep = 23, OnAdvance = () => - { //go to church + { + //go to church var world = Game.GetWorld(WorldSno.trout_town); ListenProximity(ActorSno._trdun_cath_cathedraldoorexterior, new Advance()); var leah = world.GetActorBySNO(ActorSno._leah); if (leah != null) + { leah.Hidden = false; + leah.SetVisible(true); + } SetActorVisible(world, ActorSno._tristram_mayor, false); var cart = world.GetActorBySNO(ActorSno._trout_newtristram_blocking_cart, true); if (cart != null) @@ -482,6 +494,7 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem OnAdvance = () => { //go with Cain Game.CurrentEncounter.Activated = false; + StartConversation(Game.GetWorld(WorldSno.trdun_cain_intro), 72496); ListenTeleport(19938, new Advance()); } @@ -504,19 +517,17 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem StartConversation(tristramWorld, 72498); }); //StartConversation(this.Game.GetWorld(71150), 72496); - var leah = tristramWorld.GetActorBySNO(ActorSno._leah, true); - if (leah == null) + DestroyFollower(ActorSno._leah); + + var leah = tristramWorld.GetActorsBySNO(ActorSno._leah); + if (!leah.Any()) { - leah = tristramWorld.GetActorBySNO(ActorSno._leah, false); - if (leah != null) - { - leah.Hidden = false; - leah.SetVisible(true); - } - else - { - Logger.Warn($"Leah not found in world {tristramWorld.SNO.ToString()} - quest 72095/step 32"); - } + Logger.Warn("Leah not found in world."); + } + foreach (var l in leah) + { + l.Hidden = false; + l.SetVisible(true); } ListenConversation(198617, new Advance()); } diff --git a/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/QuestProgress.cs b/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/QuestProgress.cs index 30aaa07..157eea3 100644 --- a/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/QuestProgress.cs +++ b/src/DiIiS-NA/D3-GameServer/GSSystem/QuestSystem/QuestProgress.cs @@ -276,6 +276,12 @@ namespace DiIiS_NA.GameServer.GSSystem.QuestSystem return Game.Players.Values.First().Followers.Any(x => x.Value == sno); } + public void AddUniqueFollower(World world, ActorSno sno) + { + if (!HasFollower(sno)) + AddFollower(world, sno); + } + public void AddFollower(World world, ActorSno sno) { if (Game.Players.Count > 0) diff --git a/src/DiIiS-NA/D3-GameServer/GameModsConfig.cs b/src/DiIiS-NA/D3-GameServer/GameModsConfig.cs new file mode 100644 index 0000000..90cf18e --- /dev/null +++ b/src/DiIiS-NA/D3-GameServer/GameModsConfig.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Threading.Tasks; +using DiIiS_NA; +using DiIiS_NA.Core.Logging; +using DiIiS_NA.GameServer; +using Newtonsoft.Json; + +namespace DiIiS_NA.D3_GameServer; + +public class RateConfig +{ + public float GetDamageByDifficulty(int diff) + { + if (diff < 0) diff = 0; + if (diff > 19) diff = 19; + return !DamageByDifficulty.ContainsKey(diff) ? 1f : DamageByDifficulty[diff]; + } + public Dictionary HealthByDifficulty { get; set; } = new() + { + [0] = 1.0f, [1] = 1.0f, [2] = 1.0f, [3] = 1.0f, [4] = 1.0f, [5] = 1.0f, + [6] = 1.0f, [7] = 1.0f, [8] = 1.0f, [9] = 1.0f, [10] = 1.0f, [11] = 1.0f, + [12] = 1.0f, [13] = 1.0f, [14] = 1.0f, [15] = 1.0f, [16] = 1.0f, + [17] = 1.0f, [18] = 1.0f, [19] = 1.0f, + }; + + public Dictionary DamageByDifficulty { get; set; } = new() + { + [0] = 1.0f, [1] = 1.0f, [2] = 1.0f, [3] = 1.0f, [4] = 1.0f, [5] = 1.0f, + [6] = 1.0f, [7] = 1.0f, [8] = 1.0f, [9] = 1.0f, [10] = 1.0f, [11] = 1.0f, + [12] = 1.0f, [13] = 1.0f, [14] = 1.0f, [15] = 1.0f, [16] = 1.0f, + [17] = 1.0f, [18] = 1.0f, [19] = 1.0f, + }; + public float Experience { get; set; } = 1; + public float Gold { get; set; } = 1; + public float Drop { get; set; } = 1; + public float ChangeDrop { get; set; } = 1; +} + +public class HealthConfig +{ + public float PotionRestorePercentage { get; set; } = 60f; + public float PotionCooldown { get; set; } = 30f; + public int ResurrectionCharges { get; set; } = 3; +} + +public class HealthDamageMultiplier +{ + public float HealthMultiplier { get; set; } = 1; + public float DamageMultiplier { get; set; } = 1; +} + +public class MonsterConfig +{ + public float AttacksPerSecond { get; set; } = 1.2f; + + public float HealthMultiplier { get; set; } = 1; + public float DamageMultiplier { get; set; } = 1; +} + +public class QuestConfig +{ + public bool AutoSave { get; set; } = false; + public bool UnlockAllWaypoints { get; set; } = false; +} + +public class PlayerMultiplierConfig +{ + public ParagonConfig Strength { get; set; } = new(1f); + public ParagonConfig Dexterity { get; set; } = new(1f); + public ParagonConfig Intelligence { get; set; } = new(1f); + public ParagonConfig Vitality { get; set; } = new(1f); +} +public class PlayerConfig +{ + public PlayerMultiplierConfig Multipliers = new(); +} + +public class ItemsConfig +{ + public UnidentifiedDrop UnidentifiedDropChances { get; set; } = new(); +} + +public class UnidentifiedDrop +{ + public float HighQuality { get; set; } = 30f; + public float NormalQuality { get; set; } = 5f; +} + +public class MinimapConfig +{ + public bool ForceVisibility { get; set; } = false; +} + +public class NephalemRiftConfig +{ + public float ProgressMultiplier { get; set; } = 1f; + public bool AutoFinish { get; set; } = false; + public int AutoFinishThreshold { get; set; } = 2; + public float OrbsChance { get; set; } = 0f; +} + +public class GameModsConfig +{ + public RateConfig Rate { get; set; } = new(); + public HealthConfig Health { get; set; } = new(); + public MonsterConfig Monster { get; set; } = new(); + public HealthDamageMultiplier Boss { get; set; } = new(); + public QuestConfig Quest { get; set; } = new(); + public PlayerConfig Player { get; set; } = new(); + public ItemsConfig Items { get; set; } = new(); + public MinimapConfig Minimap { get; set; } = new(); + public NephalemRiftConfig NephalemRift { get; set; } = new(); + + private static readonly Logger Logger = LogManager.CreateLogger(); + + public GameModsConfig() {} + + static GameModsConfig() + { + CreateInstance(); + } + + public static void ReloadSettings() + { + CreateInstance(); + } + + private static readonly object InstanceCreationLock = new(); + public static GameModsConfig Instance { get; private set; } + + private static void CreateInstance() + { + lock (InstanceCreationLock) + { + if (!File.Exists("config.mods.json")) + { + Instance = CreateDefaultFile(); + } + else + { + var content = File.ReadAllText("config.mods.json"); + if (content.TryFromJson(out GameModsConfig config, out Exception ex)) + { + Logger.Success("Game mods loaded successfully!"); + Logger.Info("$[italic]$Recreating $[underline]$config.mods.json$[/]$ in order to keep the structure and with all fields...$[/]$"); + var @new = config.ToJson(Formatting.Indented); + File.WriteAllText(@"config.mods.json", @new); + Logger.Success("Game mods re-structured!"); + Instance = config; + return; + } + + Logger.Fatal("An error occured whilst loading $[white on red]$config.mods.json$[/]$ file. Please verify if the file is correct. Delete the file and try again."); + Program.Shutdown(ex); + } + } + } + + private static GameModsConfig CreateDefaultFile() + { + var migration = GameServerConfig.Instance; + Logger.Info("$[blue]$Migrating mods configuration file...$[/]$"); + GameModsConfig content = new() + { +#pragma warning disable CS0618 + + Rate = + { + Experience = migration.RateExp, + Gold = migration.RateMoney, + ChangeDrop = migration.RateChangeDrop, + Drop = migration.RateDrop + }, + Health = + { + ResurrectionCharges = migration.ResurrectionCharges, + PotionCooldown = migration.HealthPotionCooldown, + PotionRestorePercentage = migration.HealthPotionRestorePercentage + }, + Monster = + { + HealthMultiplier = migration.RateMonsterHP, + DamageMultiplier = migration.RateMonsterDMG + }, + Boss = + { + HealthMultiplier = migration.BossHealthMultiplier, + DamageMultiplier = migration.BossDamageMultiplier + }, + Quest = + { + AutoSave = migration.AutoSaveQuests, + UnlockAllWaypoints = migration.UnlockAllWaypoints + }, + Player = + { + Multipliers = + { + Strength = new(migration.StrengthMultiplier, migration.StrengthParagonMultiplier), + Dexterity = new(migration.DexterityMultiplier, migration.DexterityParagonMultiplier), + Intelligence = new(migration.IntelligenceMultiplier, migration.IntelligenceParagonMultiplier), + Vitality = new(migration.VitalityMultiplier, migration.VitalityParagonMultiplier) + } + }, + Items = + { + UnidentifiedDropChances = + { + HighQuality = migration.ChanceHighQualityUnidentified, + NormalQuality = migration.ChanceNormalUnidentified + } + }, + Minimap = + { + ForceVisibility = migration.ForceMinimapVisibility + }, + NephalemRift = + { + AutoFinish = migration.NephalemRiftAutoFinish, + AutoFinishThreshold = migration.NephalemRiftAutoFinishThreshold, + OrbsChance = migration.NephalemRiftAutoFinishThreshold, + ProgressMultiplier = migration.NephalemRiftProgressMultiplier + } +#pragma warning restore CS0618 + }; + File.WriteAllText("config.mods.json", content.ToJson()); + + if (Program.Build == 30 && Program.Stage < 6) + { + Logger.Success( + "$[underline]$Migration is complete!$[/]$ - All game mods migrated from $[white]$config.ini$[/]$ to $[white]$config.mods.json$[/]$."); + } + + return content; + } +} + +public static class JsonExtensions +{ + private const bool Indented = true; + + public static string ToJson(this object obj, Formatting? formatting = null) + { + return JsonConvert.SerializeObject(obj, formatting ?? (Indented ? Formatting.Indented : Formatting.None)); + } + + public static bool TryFromJson(this string obj, out T value) + where T: class, new() + { + try + { + value = obj.FromJson(); + return true; + } + catch (Exception ex) + { + value = default; + return false; + } + } + + public static bool TryFromJson(this string obj, out T value, out Exception exception) + where T: class, new() + { + try + { + value = obj.FromJson(); + exception = null; + return true; + } + catch (Exception ex) + { + value = default; + exception = ex; + return false; + } + } + + public static T FromJson(this string obj) + where T: class, new() + { + return JsonConvert.DeserializeObject(obj); + } + + public static dynamic FromJsonDynamic(this string obj) + { + return obj.FromJson(); + } +} \ No newline at end of file diff --git a/src/DiIiS-NA/D3-GameServer/GameServerConfig.cs b/src/DiIiS-NA/D3-GameServer/GameServerConfig.cs index bce9504..cdbb2e2 100644 --- a/src/DiIiS-NA/D3-GameServer/GameServerConfig.cs +++ b/src/DiIiS-NA/D3-GameServer/GameServerConfig.cs @@ -65,21 +65,13 @@ namespace DiIiS_NA.GameServer #endif set => Set(nameof(AfkDisconnect), value); } - - /// - /// Always send motd when world loads for player. - /// - public bool MotdWhenWorldLoads - { - get => GetBoolean(nameof(MotdWhenWorldLoads), true); - set => Set(nameof(MotdWhenWorldLoads), value); - } - + #region Game Mods /// /// Rate of experience gain. /// + [Obsolete("Use GameModsConfig instead.")] public float RateExp { get => GetFloat(nameof(RateExp), 1); @@ -89,6 +81,7 @@ namespace DiIiS_NA.GameServer /// /// Rate of gold gain. /// + [Obsolete("Use GameModsConfig instead.")] public float RateMoney { get => GetFloat(nameof(RateMoney), 1); @@ -98,12 +91,14 @@ namespace DiIiS_NA.GameServer /// /// Rate of item drop. /// + [Obsolete("Use GameModsConfig instead.")] public float RateDrop { get => GetFloat(nameof(RateDrop), 1); set => Set(nameof(RateDrop), value); } + [Obsolete("Use GameModsConfig instead.")] public float RateChangeDrop { get => GetFloat(nameof(RateChangeDrop), 1); @@ -113,6 +108,7 @@ namespace DiIiS_NA.GameServer /// /// Rate of monster's HP. /// + [Obsolete("Use GameModsConfig instead.")] public float RateMonsterHP { get => GetFloat(nameof(RateMonsterHP), 1); @@ -122,6 +118,7 @@ namespace DiIiS_NA.GameServer /// /// Rate of monster's damage. /// + [Obsolete("Use GameModsConfig instead.")] public float RateMonsterDMG { get => GetFloat(nameof(RateMonsterDMG), 1); @@ -131,6 +128,7 @@ namespace DiIiS_NA.GameServer /// /// Percentage that a unique, legendary, set or special item created is unidentified /// + [Obsolete("Use GameModsConfig instead.")] public float ChanceHighQualityUnidentified { get => GetFloat(nameof(ChanceHighQualityUnidentified), 30f); @@ -140,6 +138,7 @@ namespace DiIiS_NA.GameServer /// /// Percentage that a normal item created is unidentified /// + [Obsolete("Use GameModsConfig instead.")] public float ChanceNormalUnidentified { get => GetFloat(nameof(ChanceNormalUnidentified), 5f); @@ -149,6 +148,7 @@ namespace DiIiS_NA.GameServer /// /// Resurrection charges on changing worlds /// + [Obsolete("Use GameModsConfig instead.")] public int ResurrectionCharges { get => GetInt(nameof(ResurrectionCharges), 3); @@ -158,6 +158,7 @@ namespace DiIiS_NA.GameServer /// /// Boss Health Multiplier /// + [Obsolete("Use GameModsConfig instead.")] public float BossHealthMultiplier { get => GetFloat(nameof(BossHealthMultiplier), 6f); @@ -167,6 +168,7 @@ namespace DiIiS_NA.GameServer /// /// Boss Damage Multiplier /// + [Obsolete("Use GameModsConfig instead.")] public float BossDamageMultiplier { get => GetFloat(nameof(BossDamageMultiplier), 3f); @@ -176,6 +178,7 @@ namespace DiIiS_NA.GameServer /// /// Whether to bypass the quest's settings of "Saveable" to TRUE (unless in OpenWorld) /// + [Obsolete("Use GameModsConfig instead.")] public bool AutoSaveQuests { get => GetBoolean(nameof(AutoSaveQuests), false); @@ -185,6 +188,7 @@ namespace DiIiS_NA.GameServer /// /// Progress gained when killing a monster in Nephalem Rifts /// + [Obsolete("Use GameModsConfig instead.")] public float NephalemRiftProgressMultiplier { get => GetFloat(nameof(NephalemRiftProgressMultiplier), 1f); @@ -194,6 +198,7 @@ namespace DiIiS_NA.GameServer /// /// How much a health potion heals in percentage /// + [Obsolete("Use GameModsConfig instead.")] public float HealthPotionRestorePercentage { get => GetFloat(nameof(HealthPotionRestorePercentage), 60f); @@ -203,6 +208,7 @@ namespace DiIiS_NA.GameServer /// /// Cooldown (in seconds) to use a health potion again. /// + [Obsolete("Use GameModsConfig instead.")] public float HealthPotionCooldown { get => GetFloat(nameof(HealthPotionCooldown), 30f); @@ -212,6 +218,7 @@ namespace DiIiS_NA.GameServer /// /// Unlocks all waypoints in the campaign. /// + [Obsolete("Use GameModsConfig instead.")] public bool UnlockAllWaypoints { get => GetBoolean(nameof(UnlockAllWaypoints), false); @@ -221,6 +228,7 @@ namespace DiIiS_NA.GameServer /// /// Strength multiplier when you're not a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float StrengthMultiplier { get => GetFloat(nameof(StrengthMultiplier), 1f); @@ -230,6 +238,7 @@ namespace DiIiS_NA.GameServer /// /// Strength multiplier when you're a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float StrengthParagonMultiplier { get => GetFloat(nameof(StrengthParagonMultiplier), 1f); @@ -239,6 +248,7 @@ namespace DiIiS_NA.GameServer /// /// Dexterity multiplier when you're not a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float DexterityMultiplier { get => GetFloat(nameof(DexterityMultiplier), 1f); @@ -248,6 +258,7 @@ namespace DiIiS_NA.GameServer /// /// Dexterity multiplier when you're a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float DexterityParagonMultiplier { get => GetFloat(nameof(DexterityParagonMultiplier), 1f); @@ -257,6 +268,7 @@ namespace DiIiS_NA.GameServer /// /// Intelligence multiplier when you're not a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float IntelligenceMultiplier { get => GetFloat(nameof(IntelligenceMultiplier), 1f); @@ -266,6 +278,7 @@ namespace DiIiS_NA.GameServer /// /// Intelligence multiplier when you're a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float IntelligenceParagonMultiplier { get => GetFloat(nameof(IntelligenceParagonMultiplier), 1f); @@ -275,6 +288,7 @@ namespace DiIiS_NA.GameServer /// /// Vitality multiplier when you're not a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float VitalityMultiplier { get => GetFloat(nameof(VitalityMultiplier), 1f); @@ -284,6 +298,7 @@ namespace DiIiS_NA.GameServer /// /// Vitality multiplier when you're a paragon. /// + [Obsolete("Use GameModsConfig instead.")] public float VitalityParagonMultiplier { get => GetFloat(nameof(VitalityParagonMultiplier), 1f); @@ -293,6 +308,7 @@ namespace DiIiS_NA.GameServer /// /// Auto finishes nephalem rift when there's or less monsters left. /// + [Obsolete("Use GameModsConfig instead.")] public bool NephalemRiftAutoFinish { get => GetBoolean(nameof(NephalemRiftAutoFinish), false); @@ -302,6 +318,7 @@ namespace DiIiS_NA.GameServer /// /// If is enabled, this is the threshold. /// + [Obsolete("Use GameModsConfig instead.")] public int NephalemRiftAutoFinishThreshold { get => GetInt(nameof(NephalemRiftAutoFinishThreshold), 2); @@ -311,6 +328,7 @@ namespace DiIiS_NA.GameServer /// /// Nephalem Rifts chance of spawning a orb. /// + [Obsolete("Use GameModsConfig instead.")] public float NephalemRiftOrbsChance { get => GetFloat(nameof(NephalemRiftOrbsChance), 0f); @@ -320,6 +338,7 @@ namespace DiIiS_NA.GameServer /// /// Forces the game to reveal all the map. /// + [Obsolete("Use GameModsConfig instead.")] public bool ForceMinimapVisibility { get => GetBoolean(nameof(ForceMinimapVisibility), false); @@ -327,6 +346,7 @@ namespace DiIiS_NA.GameServer } #endregion + public static GameServerConfig Instance { get; } = new(); private GameServerConfig() : base("Game-Server") diff --git a/src/DiIiS-NA/D3-GameServer/ParagonMod.cs b/src/DiIiS-NA/D3-GameServer/ParagonMod.cs new file mode 100644 index 0000000..7732c58 --- /dev/null +++ b/src/DiIiS-NA/D3-GameServer/ParagonMod.cs @@ -0,0 +1,16 @@ +public class ParagonConfig +{ + public T Normal { get; set; } + public T Paragon { get; set; } + + public ParagonConfig() {} + public ParagonConfig(T defaultValue) : this(defaultValue, defaultValue) + { + } + + public ParagonConfig(T normal, T paragon) + { + Normal = normal; + Paragon = paragon; + } +} \ No newline at end of file diff --git a/src/DiIiS-NA/Program.cs b/src/DiIiS-NA/Program.cs index f945d21..1458034 100644 --- a/src/DiIiS-NA/Program.cs +++ b/src/DiIiS-NA/Program.cs @@ -33,6 +33,7 @@ using System.Security.Permissions; using System.Threading; using System.Threading.Tasks; using DiIiS_NA.Core.Extensions; +using DiIiS_NA.D3_GameServer; using Spectre.Console; using Environment = System.Environment; @@ -65,11 +66,19 @@ namespace DiIiS_NA public static string RestServerIp = RestConfig.Instance.IP; public static string PublicGameServerIp = DiIiS_NA.GameServer.NATConfig.Instance.PublicIP; - public static int Build => 30; - public static int Stage => 2; + public const int Build = 30; + public const int Stage = 3; public static TypeBuildEnum TypeBuild => TypeBuildEnum.Beta; private static bool DiabloCoreEnabled = DiIiS_NA.GameServer.GameServerConfig.Instance.CoreActive; + private static readonly CancellationTokenSource CancellationTokenSource = new(); + public static readonly CancellationToken Token = CancellationTokenSource.Token; + public static void Cancel() => CancellationTokenSource.Cancel(); + public static void CancelAfter(TimeSpan span) => CancellationTokenSource.CancelAfter(span); + public static bool IsCancellationRequested() => CancellationTokenSource.IsCancellationRequested; + + public void MergeCancellationWith(params CancellationToken[] tokens) => + CancellationTokenSource.CreateLinkedTokenSource(tokens); static void WriteBanner() { void RightTextRule(string text, string ruleStyle) => AnsiConsole.Write(new Rule(text).RuleStyle(ruleStyle)); @@ -104,7 +113,7 @@ namespace DiIiS_NA if (!DiabloCoreEnabled) Logger.Warning("Diablo III Core is $[red]$disabled$[/]$."); #endif - + var mod = GameModsConfig.Instance; #pragma warning disable CS4014 Task.Run(async () => #pragma warning restore CS4014 @@ -126,6 +135,9 @@ namespace DiIiS_NA $"Memory: {totalMemory:0.000} GB | " + $"CPU Time: {cpuTime.ToSmallText()} | " + $"Uptime: {uptime.ToSmallText()}"; + + if (IsCancellationRequested()) + text = "SHUTTING DOWN: " + text; if (SetTitle(text)) await Task.Delay(1000); else @@ -242,15 +254,15 @@ namespace DiIiS_NA IChannel boundChannel = await serverBootstrap.BindAsync(loginConfig.Port); - Logger.Info( - "$[bold red3_1]$Tip:$[/]$ graceful shutdown with $[red3_1]$CTRL+C$[/]$ or $[red3_1]$!q[uit]$[/]$ or $[red3_1]$!exit$[/]$."); - Logger.Info("$[bold red3_1]$" + - "Tip:$[/]$ SNO breakdown with $[red3_1]$!sno$[/]$ $[red3_1]$$[/]$."); - while (true) + Logger.Info("$[bold deeppink4]$Gracefully$[/]$ shutdown with $[red3_1]$CTRL+C$[/]$ or $[deeppink4]$!q[uit]$[/]$."); + while (!IsCancellationRequested()) { var line = Console.ReadLine(); if (line is null or "!q" or "!quit" or "!exit") + { break; + } + if (line is "!cls" or "!clear" or "cls" or "clear") { AnsiConsole.Clear(); @@ -272,9 +284,11 @@ namespace DiIiS_NA if (PlayerManager.OnlinePlayers.Count > 0) { + Logger.Success("Gracefully shutting down..."); Logger.Info( $"Server is shutting down in 1 minute, $[blue]${PlayerManager.OnlinePlayers.Count} players$[/]$ are still online."); PlayerManager.SendWhisper("Server is shutting down in 1 minute."); + await Task.Delay(TimeSpan.FromMinutes(1)); } @@ -292,35 +306,39 @@ namespace DiIiS_NA } } - private static void Shutdown(Exception exception = null) + private static bool _shuttingDown = false; + public static void Shutdown(Exception exception = null) { - // if (!IsTargetEnabled("ansi")) + if (_shuttingDown) return; + _shuttingDown = true; + if (!IsCancellationRequested()) + Cancel(); + + AnsiTarget.StopIfRunning(IsTargetEnabled("ansi")); + if (exception != null) { - AnsiTarget.StopIfRunning(IsTargetEnabled("ansi")); - if (exception != null) - { - AnsiConsole.WriteLine("An unhandled exception occured at initialization. Please report this to the developers."); - AnsiConsole.WriteException(exception); - } - AnsiConsole.Progress().Start(ctx => - { - var task = ctx.AddTask("[darkred_1]Shutting down[/] [white]in[/] [red underline]10 seconds[/]"); - for (int i = 1; i < 11; i++) - { - task.Description = $"[darkred_1]Shutting down[/] [white]in[/] [red underline]{11 - i} seconds[/]"; - for (int j = 0; j < 10; j++) - { - task.Increment(1); - Thread.Sleep(100); - - } - } - - task.Description = $"[darkred_1]Shutting down[/]"; - - task.StopTask(); - }); + AnsiConsole.WriteLine( + "An unhandled exception occured at initialization. Please report this to the developers."); + AnsiConsole.WriteException(exception); } + + AnsiConsole.Progress().Start(ctx => + { + var task = ctx.AddTask("[darkred_1]Shutting down[/] [white]in[/] [red underline]10 seconds[/]"); + for (int i = 1; i < 11; i++) + { + task.Description = $"[darkred_1]Shutting down[/] [white]in[/] [red underline]{11 - i} seconds[/]"; + for (int j = 0; j < 10; j++) + { + task.Increment(1); + Thread.Sleep(100); + } + } + + task.Description = $"[darkred_1]Shutting down now.[/]"; + task.StopTask(); + }); + Environment.Exit(exception is null ? 0 : -1); } diff --git a/src/DiIiS-NA/REST/SocketBase.cs b/src/DiIiS-NA/REST/SocketBase.cs index 148381a..fbaa7ca 100644 --- a/src/DiIiS-NA/REST/SocketBase.cs +++ b/src/DiIiS-NA/REST/SocketBase.cs @@ -45,16 +45,14 @@ namespace DiIiS_NA.REST try { - using (var socketEventargs = new SocketAsyncEventArgs()) - { - socketEventargs.SetBuffer(_receiveBuffer, 0, _receiveBuffer.Length); - socketEventargs.Completed += (sender, args) => ReadHandlerInternal(args); - socketEventargs.SocketFlags = SocketFlags.None; - socketEventargs.RemoteEndPoint = _socket.RemoteEndPoint; + using var socketEventArgs = new SocketAsyncEventArgs(); + socketEventArgs.SetBuffer(_receiveBuffer, 0, _receiveBuffer.Length); + socketEventArgs.Completed += (sender, args) => ReadHandlerInternal(args); + socketEventArgs.SocketFlags = SocketFlags.None; + socketEventArgs.RemoteEndPoint = _socket.RemoteEndPoint; - if (!_socket.ReceiveAsync(socketEventargs)) - ReadHandlerInternal(socketEventargs); - } + if (!_socket.ReceiveAsync(socketEventArgs)) + ReadHandlerInternal(socketEventArgs); } catch (Exception ex) { diff --git a/src/DiIiS-NA/config.ini b/src/DiIiS-NA/config.ini index 764b0b4..a6edadb 100644 --- a/src/DiIiS-NA/config.ini +++ b/src/DiIiS-NA/config.ini @@ -1,69 +1,118 @@ -; Settings for Bnet -[Battle-Server] -Enabled = true -BindIP = 127.0.0.1 -WebPort = 9800 -Port = 1119 -BindIPv6 = ::1 -MotdEnabled = true -Motd = Welcome to Blizzless Diablo 3! +; ========== +; Configuration File Template +; ========== +; This is a template configuration file which can be modified as desired. The following branches are available for your convenience: +; - Community branch (recommended): https://github.com/blizzless/blizzless-diiis/tree/community +; - Test-stable branch: https://github.com/blizzless/blizzless-diiis/ +; - Master branch: https://github.com/blizzless/blizzless-diiis/tree/master -; ------------------------ +; Battle Server Settings +[Battle-Server] +; Enable or disable the Battle Server +Enabled = true +; IP address on which the server will be bound +BindIP = 127.0.0.1 +; Port for web interactions +WebPort = 9800 +; Port for the server +Port = 1119 + +; Message of the Day (MotD) Settings +; - MotdEnabled: Toggles whether the Message of The Day (MotD) is enabled or not +; - MotdEnabledWhenWorldLoads: Determines if MotD should be displayed every time a new world is loaded for a player +; - Motd: Text displayed as the MotD +MotdEnabled = true +MotdEnabledWhenWorldLoads = false +Motd = Welcome to Blizzless D3! +; - Remote MotD Enabled: Enable receiving MotD from a remote URL via POST request with payload: { "GameAccountId": ulong, "ToonName": string, "WorldGlobalId": uint } +; - MotdRemoteUrl: Remote URL to send payload and receive string; falls back to Motd string if unavailable +MotdEnabledRemote = false +MotdRemoteUrl = https://your-site.local/yourmotd + +; IWServer Setting (Currently inactive) ; [IWServer] ; IWServer = false -; ------------------------ -; REST services for login (and others) +; REST Service Settings for Login and Other Functions [REST] IP = 127.0.0.1 -Public = true PublicIP = 127.0.0.1 PORT = 80 +Public = true -; ------------------------ -; Game server options and game-mods. +; Game Server Settings [Game-Server] +; Enable or disable the game server Enabled = true +; Activate game server core functionality CoreActive = true +; IP address on which the game server will be bound BindIP = 127.0.0.1 +; Port for web interactions WebPort = 9001 +; Port for game server connections Port = 1345 +; IP address for IPv6 bindings BindIPv6 = ::1 +; DRLG Emulation status DRLGemu = true -; Modding of game + +; NAT (Network Address Translation) Settings +[NAT] +; Toggles the NAT functionality +Enabled = True +; Your public IP address to enable NAT +PublicIP = 127.0.0.1 + +; ========== +; Game Modding Configuration +; For documentation, please check https://github.com/blizzless/blizzless-diiis/blob/community/docs/game-world-settings.md +; Multipliers for various gameplay rates RateExp = 1 RateMoney = 1 RateDrop = 1 RateChangeDrop = 1 RateMonsterHP = 1 RateMonsterDMG = 1 -; Percentage that a unique, legendary, set or special item created is unidentified -ChanceHighQualityUnidentified = 80 -; Percentage that normal item created is unidentified + +; Quality and identification chances for items +ChanceHighQualityUnidentified = 30 ChanceNormalUnidentified = 5 -; Amount of times user can resurrect at corpse -ResurrectionCharges = 5 -BossHealthMultiplier = 2 -BossDamageMultiplier = 1 -AutoSaveQuests = true -; ------------------------ -; Network address translation -[NAT] -Enabled = True -PublicIP = 127.0.0.1 +; Boss health and damage multipliers +BossHealthMultiplier = 6 +BossDamageMultiplier = 3 -; ------------------------ -; Where the outputs should be. -; Best for visualization (default): AnsiLog (target: Ansi) -; Best for debugging: ConsoleLog (target: console) -; Best for packet analysis: PacketLog (target: file) -; Logging level (ordered): -; Rarely used: RenameAccountLog (0), ChatMessage (1), BotCommand (2), -; Useful: Debug (3), MethodTrace (4), Trace (5), -; Normal and human-readable: Info (6), Success (7), -; Errors: Warn (8), Error (9), Fatal (10), -; Network Logs: PacketDump (11) +; Nephalem Rift progress multiplier +NephalemRiftProgressMultiplier = 1 + +; Health potion mechanics +HealthPotionRestorePercentage = 60 +HealthPotionCooldown = 30 +ResurrectionCharges = 3 + +; Waypoint settings +UnlockAllWaypoints = false + +; Player attribute modifiers +StrengthMultiplier = 1 +StrengthParagonMultiplier = 1 +DexterityMultiplier = 1 +DexterityParagonMultiplier = 1 +IntelligenceMultiplier = 1 +IntelligenceParagonMultiplier = 1 +VitalityMultiplier = 1 +VitalityParagonMultiplier = 1 + +; Quest saving behavior +AutoSaveQuests = false + +; Minimap visibility settings +ForceMinimapVisibility = false + +; =================== +; Log Output Settings +; AnsiLog for visualization, ConsoleLog for debugging, and PacketLog for packet analysis [AnsiLog] Enabled = true @@ -76,8 +125,8 @@ MaximumLevel = Fatal Enabled = false Target = Console IncludeTimeStamps = true -MinimumLevel = Debug -MaximumLevel = PacketDump +MinimumLevel = MethodTrace +MaximumLevel = Fatal [PacketLog] Enabled = true diff --git a/src/DiIiS-NA/database.Account.config b/src/DiIiS-NA/database.Account.config index 9de7d15..1f16a9e 100644 --- a/src/DiIiS-NA/database.Account.config +++ b/src/DiIiS-NA/database.Account.config @@ -8,7 +8,7 @@ true 0 - Server=localhost;Database=diiis;User ID=postgres;Password=postgres + Server=localhost;Database=diiis;User ID=postgres;Password=password on_close 0 @@ -16,4 +16,4 @@ false false - \ No newline at end of file + diff --git a/src/DiIiS-NA/database.Worlds.config b/src/DiIiS-NA/database.Worlds.config index 8545182..2f11a7b 100644 --- a/src/DiIiS-NA/database.Worlds.config +++ b/src/DiIiS-NA/database.Worlds.config @@ -7,7 +7,7 @@ true 0 - Server=localhost;Database=worlds;User ID=postgres;Password=postgres + Server=localhost;Database=worlds;User ID=postgres;Password=password on_close 0 @@ -15,4 +15,4 @@ false false - \ No newline at end of file +