Merge remote-tracking branch 'origin/community' into community

This commit is contained in:
Lucca Faria Ferri 2023-08-29 08:55:58 +02:00
commit cabbc45654
45 changed files with 1582 additions and 965 deletions

View File

@ -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

View File

@ -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

60
configs/config.mods.json Normal file
View File

@ -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
}
}

View File

@ -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
}
}
```

View File

@ -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();

View File

@ -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,11 +503,42 @@ namespace DiIiS_NA.LoginServer.Battle
}
public void SendMotd()
{
if (string.IsNullOrWhiteSpace(LoginServerConfig.Instance.Motd) || !LoginServerConfig.Instance.MotdEnabled)
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)
{

View File

@ -48,6 +48,12 @@ namespace DiIiS_NA.LoginServer
set => Set(nameof(MotdEnabled), value);
}
public bool MotdEnabledWhenWorldLoads
{
get => GetBoolean(nameof(MotdEnabledWhenWorldLoads), false);
set => Set(nameof(MotdEnabledWhenWorldLoads), value);
}
/// <summary>
/// Motd text
/// </summary>
@ -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")

View File

@ -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);
}

View File

@ -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<string, string> _replacements = new ()
{
["["] = "[[",
["]"] = "]]",
["$[[/]]$"] = "[/]",
["$[["] = "[",
["]]$"] = "]"
};
/// <summary>
/// Performs a cleanup on the target.
/// All [ becomes [[, and ] becomes ]] (for ignoring ANSI codes)
@ -112,82 +123,58 @@ public class AnsiTarget : LogTarget
/// </summary>
/// <param name="x"></param>
/// <returns></returns>
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)
{
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);
@ -196,40 +183,25 @@ public class AnsiTarget : LogTarget
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());
return message;
}
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());
}
}
}
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() + "$[/]$";
}

View File

@ -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.
/// </summary>
/// <returns>A <see cref="Logger"/> instance.</returns>
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.
}
/// <summary>
@ -41,10 +42,10 @@ namespace DiIiS_NA.Core.Logging
/// </summary>
/// <param name="name"></param>
/// <returns>A <see cref="Logger"/> instance.</returns>
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.
}

View File

@ -13,15 +13,17 @@ namespace DiIiS_NA.Core.Logging
public class Logger
{
public string Name { get; protected set; }
public string FilePath { get; protected set; }
/// <summary>
/// A logger base type is used to create a logger instance.
/// E.g. ConsoleTarget, FileTarget, etc.
/// </summary>
/// <param name="name">Logger name</param>
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).
/// </summary>
Fatal,
/// <summary>
/// The messages meant for quest general logging purposes.
/// </summary>
QuestInfo,
/// <summary>
/// The messages meant for quest logging purposes.
/// </summary>
QuestStep,
/// <summary>
/// Packet messages.
/// </summary>
@ -105,14 +116,39 @@ namespace DiIiS_NA.Core.Logging
/// <param name="message">The log message.</param>
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
}
/// <param name="message">The log message.</param>
public void Debug(string message) => Log(Level.Debug, message, null);

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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"))

View File

@ -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!");
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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.

View File

@ -23,7 +23,8 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
{
public class MonsterBrain : Brain
{
private new readonly Logger Logger;
private new readonly Logger _logger;
// list of power SNOs that are defined for the monster
public Dictionary<int, Cooldown> PresetPowers { get; private set; }
@ -36,9 +37,9 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
}
private bool _warnedNoPowers;
private Actor _target { get; set; }
private Actor Target { get; set; }
private int _mpqPowerCount;
private bool Feared = false;
private bool _feared = false;
public Actor AttackedBy = null;
public TickTimer TimeoutAttacked = null;
@ -48,30 +49,33 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
public MonsterBrain(Actor body)
: base(body)
{
Logger = LogManager.CreateLogger(GetType().Name);
_logger = LogManager.CreateLogger(GetType().Name);
PresetPowers = new Dictionary<int, Cooldown>();
// 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");
_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;
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))
{
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 });
}
PresetPowers.Add(monsterData.SkillDeclarations[i].SNOPower,
new Cooldown { CooldownTimer = null, CooldownTime = cooldownTime });
}
if (monsterData.SkillDeclarations.All(s => s.SNOPower != 30592))
PresetPowers.Add(30592, new Cooldown { CooldownTimer = null, CooldownTime = 0f }); //hack for dummy mobs without powers
PresetPowers.Add(30592,
new Cooldown { CooldownTimer = null, CooldownTime = 0f }); //hack for dummy mobs without powers
}
public override void Think(int tickCounter)
@ -88,7 +92,8 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
if (Body.Hidden)
return;
if (CurrentAction != null && PriorityTarget != null && PriorityTarget.Attributes[GameAttributes.Is_Helper] == true)
if (CurrentAction != null && PriorityTarget != null &&
PriorityTarget.Attributes[GameAttributes.Is_Helper] == true)
{
PriorityTarget = null;
CurrentAction.Cancel(tickCounter);
@ -118,6 +123,7 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
CurrentAction.Cancel(tickCounter);
CurrentAction = null;
}
_powerDelay = null;
return;
@ -125,14 +131,14 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
if (Body.Attributes[GameAttributes.Feared])
{
if (!Feared || CurrentAction == null)
{
if (_feared && CurrentAction != null) return;
if (CurrentAction != null)
{
CurrentAction.Cancel(tickCounter);
CurrentAction = null;
}
Feared = true;
_feared = true;
CurrentAction = new MoveToPointWithPathfindAction(
Body,
PowerContext.RandomDirection(Body.Position, 3f, 8f)
@ -140,35 +146,40 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
return;
}
return;
}
_feared = false;
Feared = false;
if (CurrentAction == null)
{
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<Player>(50f).Count != 0)
{
if (_powerDelay.TimedOut)
{
// If the power delay hasn't timed out, return
if (!_powerDelay.TimedOut) return;
// Reset the power delay
_powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f);
// If the character has been attacked, set the attacker as the priority target
if (AttackedBy != null)
PriorityTarget = AttackedBy;
// If there's no defined priority target, start a search
if (PriorityTarget == null)
{
Actor[] targets;
// 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<Monster>(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<ActorGhostedBuff>(p) == null)
.Where(p => ((p is Player) && !p.Dead && p.Attributes[GameAttributes.Loading] == false &&
p.Attributes[GameAttributes.Is_Helper] == false &&
p.World.BuffManager.GetFirstBuff<ActorGhostedBuff>(p) == null)
|| ((p is Minion) && !p.Dead && p.Attributes[GameAttributes.Is_Helper] == false)
|| (p is DesctructibleLootContainer && p.SNO.IsDoorOrBarricade())
|| ((p is Hireling) && !p.Dead)
@ -176,55 +187,74 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
.OrderBy((actor) => PowerMath.Distance2D(actor.Position, Body.Position))
.ToArray();
// If there are no targets, return
if (targets.Length == 0) return;
_target = targets.First();
// Set the first found target as the target
Target = targets.First();
}
else
_target = PriorityTarget;
// If there is a priority target, set it as the target
Target = PriorityTarget;
int powerToUse = PickPowerToUse();
if (powerToUse > 0)
{
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)
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);
Body.TranslateFacing(Target.Position, false);
CurrentAction = new PowerAction(Body, powerToUse, _target);
CurrentAction = new PowerAction(Body, powerToUse, Target);
if (power is SummoningSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) };
if (power is MonsterAffixSkill monsterAffixSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime };
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]
};
if (PresetPowers[powerToUse].CooldownTime > 0f)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime };
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 };
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}]");
_logger.Trace(
$"{GetType().Name} $[underline white]${nameof(MoveToPointAction)}$[/]$ to target $[white]${Target.ActorType}$[/]$ [{Target.Position}]");
CurrentAction = new MoveToPointAction(
Body, _target.Position
Body, Target.Position
);
}
else
{
Logger.Trace($"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}");
_logger.Trace(
$"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}");
CurrentAction = new MoveToTargetWithPathfindAction(
Body,
_target,
attackRange + _target.ActorData.Cylinder.Ax2,
Target,
attackRange + Target.ActorData.Cylinder.Ax2,
powerToUse
);
}
@ -236,29 +266,38 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
ActorSno._a1dun_leor_firewall2 => 223284,
_ => powerToUse
};
CurrentAction = new PowerAction(Body, powerToUse, _target);
CurrentAction = new PowerAction(Body, powerToUse, Target);
if (power is SummoningSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) };
if (power is MonsterAffixSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (power as MonsterAffixSkill).CooldownTime };
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 (PresetPowers[powerToUse].CooldownTime > 0f)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime };
PresetPowers[powerToUse] = new Cooldown
{
CooldownTimer =
new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime),
CooldownTime = PresetPowers[powerToUse].CooldownTime
};
if (powerToUse == 96925 ||
powerToUse == 223284)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f };
}
}
if (powerToUse is 96925 or 223284)
PresetPowers[powerToUse] = new Cooldown
{ CooldownTimer = new SecondsTickTimer(Body.World.Game, 10f), CooldownTime = 10f };
}
}
else if (Body.GetObjectsInRange<Living>(50f).Count != 0)
{
if (_powerDelay.TimedOut)
{
if (!_powerDelay.TimedOut) return;
_powerDelay = new SecondsTickTimer(Body.World.Game, 1.0f);
if (AttackedBy != null)
@ -287,28 +326,33 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
if (targets.Length == 0)
return;
foreach (var monsterActor in targets.Where(tar => _target == null))
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;
Target = brain.AttackedBy;
}
else
{
_target = targets.First();
Target = targets.First();
}
foreach (var tar in targets)
if (tar is DesctructibleLootContainer && tar.SNO.IsDoorOrBarricade() && tar.SNO != ActorSno._trout_wagon_barricade)
{ _target = tar; break; }
if (tar is DesctructibleLootContainer && tar.SNO.IsDoorOrBarricade() &&
tar.SNO != ActorSno._trout_wagon_barricade)
{
Target = tar;
break;
}
}
else
_target = PriorityTarget;
Target = PriorityTarget;
int powerToUse = PickPowerToUse();
if (powerToUse > 0)
{
PowerScript power = PowerLoader.CreateImplementationForPowerSNO(powerToUse);
power.User = Body;
if (_target == null)
if (Target == null)
{
/*
if (!this.Body.ActorSNO.Name.ToLower().Contains("woodwraith") &&
@ -322,59 +366,77 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
//*/
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)
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
Body.TranslateFacing(Target.Position,
false); //columns and other non-walkable shit can't turn
Logger.Trace($"{GetType().Name} {nameof(PowerAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}");
_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 PowerAction(Body, powerToUse, Target);
if (power is SummoningSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) };
if (power is MonsterAffixSkill monsterSkill)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = null, CooldownTime = monsterSkill.CooldownTime };
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]
};
if (PresetPowers[powerToUse].CooldownTime > 0f)
PresetPowers[powerToUse] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[powerToUse].CooldownTime), CooldownTime = PresetPowers[powerToUse].CooldownTime };
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(MoveToPointAction)} to target [{Target.Position}]");
CurrentAction = new MoveToPointAction(
Body, _target.Position
Body, Target.Position
);
}
else
{
Logger.Trace($"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{_target.ActorType}] {_target.SNO.ToString()}");
_logger.Trace(
$"{GetType().Name} {nameof(MoveToTargetWithPathfindAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}");
CurrentAction = new MoveToTargetWithPathfindAction(
Body,
//(
_target,// + MovementHelpers.GetMovementPosition(
Target, // + MovementHelpers.GetMovementPosition(
//new Vector3D(0, 0, 0),
//this.Body.WalkSpeed,
//MovementHelpers.GetFacingAngle(_target.Position, this.Body.Position),
//6
//)
//)
attackRange + _target.ActorData.Cylinder.Ax2,
attackRange + Target.ActorData.Cylinder.Ax2,
powerToUse
);
}
}
}
}
}
else
{
@ -383,8 +445,9 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
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)
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);
@ -400,33 +463,44 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
break;
tryC++;
}
return point;
}
public void FastAttack(Actor target, int skillSNO)
public void FastAttack(Actor target, int skillSno)
{
PowerScript power = PowerLoader.CreateImplementationForPowerSNO(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);
_logger.Trace(
$"{GetType().Name} {nameof(FastAttack)} {nameof(PowerAction)} to target [{Target.ActorType}] {Target.SNO.ToString()}");
CurrentAction = new PowerAction(Body, skillSno, target);
if (power is SummoningSkill)
PresetPowers[skillSNO] = new Cooldown { CooldownTimer = null, CooldownTime = (Body is Boss ? 15f : 7f) };
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 (power is MonsterAffixSkill monsterAffixSkill)
PresetPowers[skillSNO] = new Cooldown { CooldownTimer = null, CooldownTime = monsterAffixSkill.CooldownTime };
if (PresetPowers[skillSNO].CooldownTime > 0f)
PresetPowers[skillSNO] = new Cooldown { CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[skillSNO].CooldownTime), CooldownTime = PresetPowers[skillSNO].CooldownTime };
if (PresetPowers[skillSno].CooldownTime > 0f)
PresetPowers[skillSno] = new Cooldown
{
CooldownTimer = new SecondsTickTimer(Body.World.Game, PresetPowers[skillSno].CooldownTime),
CooldownTime = PresetPowers[skillSno].CooldownTime
};
}
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.");
_logger.Warn(
$"Monster $[red]$\"{Body.Name}\"$[/]$ has no usable powers. {_mpqPowerCount} are defined in mpq data.");
_warnedNoPowers = true;
return -1;
}
@ -435,7 +509,9 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
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();
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;
@ -448,28 +524,28 @@ namespace DiIiS_NA.GameServer.GSSystem.AISystem.Brains
return -1;
}
public void AddPresetPower(int powerSNO)
public void AddPresetPower(int powerSno)
{
if (PresetPowers.ContainsKey(powerSNO))
if (PresetPowers.ContainsKey(powerSno))
{
Logger.Debug($"Monster $[red]$\"{Body.Name}\"$[/]$ already has power {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.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)
public void RemovePresetPower(int powerSno)
{
if (PresetPowers.ContainsKey(powerSNO))
if (PresetPowers.ContainsKey(powerSno))
{
PresetPowers.Remove(powerSNO);
PresetPowers.Remove(powerSno);
}
}
}

View File

@ -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;

View File

@ -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
{
}
}

View File

@ -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,15 +188,13 @@ namespace DiIiS_NA.GameServer.GSSystem.ActorSystem
lock (_adjustLock)
{
int count = player.World.Game.Players.Count;
if (count > 0 && _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;

View File

@ -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
}

View File

@ -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
/// </summary>
public ConcurrentDictionary<GameClient, Player> Players { get; private set; }
public Player FirstPlayer() => Players.Values.First();
public ImmutableArray<Player> 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.
/// </summary>
public int CurrentQuest = -1;
public int CurrentSideQuest = -1;
public bool IsCurrentOpenWorld => CurrentQuest == 312429;
/// <summary>
/// Current quest step SNOid.
/// </summary>
@ -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);

View File

@ -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));
/// <summary>
/// Accessor for quests
/// </summary>
/// <param name="snoQuest">snoId of the quest to retrieve</param>
/// <returns></returns>
public readonly Dictionary<int, QuestRegistry.Quest> Quests = new();
public readonly Dictionary<int, QuestRegistry.Quest> 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);
/// <summary>
/// Advances a quest by a step
/// </summary>
/// <param name="snoQuest">snoID of the quest to advance</param>
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);
}

View File

@ -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)

View File

@ -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<float> { 0.18f * GameServerConfig.Instance.RateChangeDrop };
return new List<float> { 0.18f * GameModsConfig.Instance.Rate.ChangeDrop };
case 1: //Champion
return new List<float> { 1f, 1f, 1f, 1f, 0.75f * GameServerConfig.Instance.RateChangeDrop };
return new List<float> { 1f, 1f, 1f, 1f, 0.75f * GameModsConfig.Instance.Rate.ChangeDrop };
case 2: //Rare (Elite)
case 4: //Unique
return new List<float> { 1f, 1f, 1f, 1f, 1f };
case 7: //Boss
return new List<float> { 1f, 1f, 1f, 1f, 1f, 0.75f * GameServerConfig.Instance.RateChangeDrop, 0.4f * GameServerConfig.Instance.RateChangeDrop };
return new List<float> { 1f, 1f, 1f, 1f, 1f, 0.75f * GameModsConfig.Instance.Rate.ChangeDrop, 0.4f * GameModsConfig.Instance.Rate.ChangeDrop };
default:
return new List<float> { 0.12f * GameServerConfig.Instance.RateChangeDrop };
return new List<float> { 0.12f * GameModsConfig.Instance.Rate.ChangeDrop };
}
}

View File

@ -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
};
}

View File

@ -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
/// <param name="position">The position for drop.</param>
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.

View File

@ -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,7 +4012,7 @@ public class Player : Actor, IMessageConsumer, IUpdateable
get
{
var baseStrength = 0.0f;
var multiplier = ParagonLevel > 0 ? GameServerConfig.Instance.StrengthParagonMultiplier : GameServerConfig.Instance.StrengthMultiplier;
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)

View File

@ -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<TickTimer> 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)));
}
}
}

View File

@ -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

View File

@ -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<TickTimer> 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<uint> { 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;
damageType = DamageType.Cold;
attack.AddWeaponDamage(Damage, DType);
// 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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -244,15 +244,22 @@ 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);
Logger.Warn("Leah not found in world.");
}
else
foreach (var l in leah)
{
Logger.Warn($"Leah not found in world {tristramWorld.SNO.ToString()} - quest 72095/step 32");
}
l.Hidden = false;
l.SetVisible(true);
}
ListenConversation(198617, new Advance());
}

View File

@ -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)

View File

@ -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<int, float> 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<int, float> 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<float> Strength { get; set; } = new(1f);
public ParagonConfig<float> Dexterity { get; set; } = new(1f);
public ParagonConfig<float> Intelligence { get; set; } = new(1f);
public ParagonConfig<float> 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<T>(this string obj, out T value)
where T: class, new()
{
try
{
value = obj.FromJson<T>();
return true;
}
catch (Exception ex)
{
value = default;
return false;
}
}
public static bool TryFromJson<T>(this string obj, out T value, out Exception exception)
where T: class, new()
{
try
{
value = obj.FromJson<T>();
exception = null;
return true;
}
catch (Exception ex)
{
value = default;
exception = ex;
return false;
}
}
public static T FromJson<T>(this string obj)
where T: class, new()
{
return JsonConvert.DeserializeObject<T>(obj);
}
public static dynamic FromJsonDynamic(this string obj)
{
return obj.FromJson<ExpandoObject>();
}
}

View File

@ -66,20 +66,12 @@ namespace DiIiS_NA.GameServer
set => Set(nameof(AfkDisconnect), value);
}
/// <summary>
/// Always send motd when world loads for player.
/// </summary>
public bool MotdWhenWorldLoads
{
get => GetBoolean(nameof(MotdWhenWorldLoads), true);
set => Set(nameof(MotdWhenWorldLoads), value);
}
#region Game Mods
/// <summary>
/// Rate of experience gain.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float RateExp
{
get => GetFloat(nameof(RateExp), 1);
@ -89,6 +81,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Rate of gold gain.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float RateMoney
{
get => GetFloat(nameof(RateMoney), 1);
@ -98,12 +91,14 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Rate of item drop.
/// </summary>
[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
/// <summary>
/// Rate of monster's HP.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float RateMonsterHP
{
get => GetFloat(nameof(RateMonsterHP), 1);
@ -122,6 +118,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Rate of monster's damage.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float RateMonsterDMG
{
get => GetFloat(nameof(RateMonsterDMG), 1);
@ -131,6 +128,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Percentage that a unique, legendary, set or special item created is unidentified
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float ChanceHighQualityUnidentified
{
get => GetFloat(nameof(ChanceHighQualityUnidentified), 30f);
@ -140,6 +138,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Percentage that a normal item created is unidentified
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float ChanceNormalUnidentified
{
get => GetFloat(nameof(ChanceNormalUnidentified), 5f);
@ -149,6 +148,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Resurrection charges on changing worlds
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public int ResurrectionCharges
{
get => GetInt(nameof(ResurrectionCharges), 3);
@ -158,6 +158,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Boss Health Multiplier
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float BossHealthMultiplier
{
get => GetFloat(nameof(BossHealthMultiplier), 6f);
@ -167,6 +168,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Boss Damage Multiplier
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float BossDamageMultiplier
{
get => GetFloat(nameof(BossDamageMultiplier), 3f);
@ -176,6 +178,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Whether to bypass the quest's settings of "Saveable" to TRUE (unless in OpenWorld)
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public bool AutoSaveQuests
{
get => GetBoolean(nameof(AutoSaveQuests), false);
@ -185,6 +188,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Progress gained when killing a monster in Nephalem Rifts
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float NephalemRiftProgressMultiplier
{
get => GetFloat(nameof(NephalemRiftProgressMultiplier), 1f);
@ -194,6 +198,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// How much a health potion heals in percentage
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float HealthPotionRestorePercentage
{
get => GetFloat(nameof(HealthPotionRestorePercentage), 60f);
@ -203,6 +208,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Cooldown (in seconds) to use a health potion again.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float HealthPotionCooldown
{
get => GetFloat(nameof(HealthPotionCooldown), 30f);
@ -212,6 +218,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Unlocks all waypoints in the campaign.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public bool UnlockAllWaypoints
{
get => GetBoolean(nameof(UnlockAllWaypoints), false);
@ -221,6 +228,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Strength multiplier when you're not a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float StrengthMultiplier
{
get => GetFloat(nameof(StrengthMultiplier), 1f);
@ -230,6 +238,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Strength multiplier when you're a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float StrengthParagonMultiplier
{
get => GetFloat(nameof(StrengthParagonMultiplier), 1f);
@ -239,6 +248,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Dexterity multiplier when you're not a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float DexterityMultiplier
{
get => GetFloat(nameof(DexterityMultiplier), 1f);
@ -248,6 +258,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Dexterity multiplier when you're a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float DexterityParagonMultiplier
{
get => GetFloat(nameof(DexterityParagonMultiplier), 1f);
@ -257,6 +268,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Intelligence multiplier when you're not a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float IntelligenceMultiplier
{
get => GetFloat(nameof(IntelligenceMultiplier), 1f);
@ -266,6 +278,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Intelligence multiplier when you're a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float IntelligenceParagonMultiplier
{
get => GetFloat(nameof(IntelligenceParagonMultiplier), 1f);
@ -275,6 +288,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Vitality multiplier when you're not a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float VitalityMultiplier
{
get => GetFloat(nameof(VitalityMultiplier), 1f);
@ -284,6 +298,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Vitality multiplier when you're a paragon.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float VitalityParagonMultiplier
{
get => GetFloat(nameof(VitalityParagonMultiplier), 1f);
@ -293,6 +308,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Auto finishes nephalem rift when there's <see cref="NephalemRiftAutoFinishThreshold"></see> or less monsters left.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public bool NephalemRiftAutoFinish
{
get => GetBoolean(nameof(NephalemRiftAutoFinish), false);
@ -302,6 +318,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// If <see cref="NephalemRiftAutoFinish"></see> is enabled, this is the threshold.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public int NephalemRiftAutoFinishThreshold
{
get => GetInt(nameof(NephalemRiftAutoFinishThreshold), 2);
@ -311,6 +328,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Nephalem Rifts chance of spawning a orb.
/// </summary>
[Obsolete("Use GameModsConfig instead.")]
public float NephalemRiftOrbsChance
{
get => GetFloat(nameof(NephalemRiftOrbsChance), 0f);
@ -320,6 +338,7 @@ namespace DiIiS_NA.GameServer
/// <summary>
/// Forces the game to reveal all the map.
/// </summary>
[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")

View File

@ -0,0 +1,16 @@
public class ParagonConfig<T>
{
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;
}
}

View File

@ -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]$<fullSnoBreakdown(true:false)>$[/]$.");
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,16 +306,22 @@ namespace DiIiS_NA
}
}
private static void Shutdown(Exception exception = null)
{
// if (!IsTargetEnabled("ansi"))
private static bool _shuttingDown = false;
public static void Shutdown(Exception exception = null)
{
if (_shuttingDown) return;
_shuttingDown = true;
if (!IsCancellationRequested())
Cancel();
AnsiTarget.StopIfRunning(IsTargetEnabled("ansi"));
if (exception != null)
{
AnsiConsole.WriteLine("An unhandled exception occured at initialization. Please report this to the developers.");
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[/]");
@ -312,15 +332,13 @@ namespace DiIiS_NA
{
task.Increment(1);
Thread.Sleep(100);
}
}
task.Description = $"[darkred_1]Shutting down[/]";
task.Description = $"[darkred_1]Shutting down now.[/]";
task.StopTask();
});
}
Environment.Exit(exception is null ? 0 : -1);
}

View File

@ -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)
{

View File

@ -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

View File

@ -8,7 +8,7 @@
<property name="use_proxy_validator">true</property>
<property name="command_timeout">0</property>
<property name="connection.connection_string">
Server=localhost;Database=diiis;User ID=postgres;Password=postgres
Server=localhost;Database=diiis;User ID=postgres;Password=password
</property>
<property name="connection.release_mode">on_close</property>
<property name="adonet.batch_size">0</property>

View File

@ -7,7 +7,7 @@
<property name="use_proxy_validator">true</property>
<property name="command_timeout">0</property>
<property name="connection.connection_string">
Server=localhost;Database=worlds;User ID=postgres;Password=postgres
Server=localhost;Database=worlds;User ID=postgres;Password=password
</property>
<property name="connection.release_mode">on_close</property>
<property name="adonet.batch_size">0</property>