ModernUO/Projects/UOContent/Engines/ExileHunterBestiary/Core/HunterBestiaryEngine.cs

567 lines
No EOL
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using Server.Commands;
using Server.Custom.ExileHunterBestiary.Gumps;
using Server.Custom.ExileHunterBestiary.Hooks;
using Server.Logging;
using Server.Mobiles;
namespace Server.Custom.ExileHunterBestiary;
public static class HunterBestiaryEngine
{
private static readonly ILogger _logger =
LogFactory.GetLogger(typeof(HunterBestiaryEngine));
private static readonly Dictionary<Serial, HunterProfile> _profiles = [];
public const double DefaultDropChance = 0.02;
public const double BossDropChance = 0.05;
public static bool Enabled { get; private set; } = true;
public static double PerCreatureBonus { get; private set; } = 0.10;
public static double CategoryMilestoneBonus { get; private set; } = 0.20;
public static double TotalBonusCap { get; private set; } = 0.25;
public static int PityThreshold { get; private set; } = 40;
public static DamageMode DamageMode { get; private set; } = DamageMode.Mark;
public static double GlobalDropChance { get; private set; } = -1.0;
public static bool Debug { get; private set; } = true;
public static void Configure()
{
Enabled = GetBool("exileBestiary.enabled", true);
PerCreatureBonus = GetDouble(
"exileBestiary.damage.perCreatureBonus",
0.10
);
CategoryMilestoneBonus = GetDouble(
"exileBestiary.damage.categoryMilestoneBonus",
0.20
);
TotalBonusCap = GetDouble(
"exileBestiary.damage.totalCap",
0.25
);
PityThreshold = GetInt(
"exileBestiary.drop.pityThreshold",
40
);
DamageMode = GetEnum(
"exileBestiary.damage.mode",
DamageMode.Mark
);
GlobalDropChance = GetDouble(
"exileBestiary.drop.chance",
-1.0
);
Debug = GetBool(
"exileBestiary.debug",
true
);
_logger.Information(
"Config ({Source}): enabled={Enabled} mode={Mode} dropChance={DropChance} perCreature={PerCreature} milestone={Milestone} cap={Cap} pity={Pity} debug={Debug}",
_configSource,
Enabled,
DamageMode,
GlobalDropChance,
PerCreatureBonus,
CategoryMilestoneBonus,
TotalBonusCap,
PityThreshold,
Debug
);
BestiaryRegistry.Initialize();
if (!Enabled)
{
_logger.Warning(
"System disabled via configuration (exileBestiary.enabled = false)."
);
return;
}
EventSink.WorldSave += OnWorldSave;
EventSink.WorldLoad += OnWorldLoad;
EventSink.AggressiveAction += static args =>
HunterCombatTracker.Note(args.Aggressor, args.Aggressed);
HunterCombatTracker.StartSweeper();
ReloadProfiles();
CommandSystem.Register(
"Bestiary",
AccessLevel.Player,
OnBestiaryCommand
);
CommandSystem.Register(
"Bestiario",
AccessLevel.Player,
OnBestiaryCommand
);
CommandSystem.Register(
"BestiaryTest",
AccessLevel.GameMaster,
OnBestiaryTestCommand
);
CommandSystem.Register(
"BestiaryDiag",
AccessLevel.GameMaster,
OnBestiaryDiagCommand
);
if (DamageMode == DamageMode.Mark)
{
HunterMarkHook.Configure();
}
}
private static void ReloadProfiles()
{
_profiles.Clear();
foreach (var (serial, profile) in HunterProfileStore.Load())
{
_profiles[serial] = profile;
}
}
public static HunterProfile GetProfile(Mobile m)
{
if (m == null)
{
return null;
}
if (!_profiles.TryGetValue(m.Serial, out var profile))
{
profile = new HunterProfile(m.Serial);
_profiles[m.Serial] = profile;
}
return profile;
}
public static bool TryGetProfile(
Mobile m,
out HunterProfile profile
)
{
if (m == null)
{
profile = null;
return false;
}
return _profiles.TryGetValue(m.Serial, out profile);
}
public static bool TryGetEffectiveBonus(
Mobile attacker,
BaseCreature victim,
out double bonus
)
{
bonus = 0;
if (!Enabled || attacker == null || victim == null)
{
return false;
}
var entry = BestiaryRegistry.FindEntry(victim.GetType());
if (entry == null ||
!_profiles.TryGetValue(attacker.Serial, out var profile))
{
return false;
}
if (profile.HasLearned(entry.Id))
{
bonus += PerCreatureBonus;
}
if (BestiaryRegistry.ByCategory.TryGetValue(
entry.Category,
out var categoryEntries
) &&
categoryEntries.Count > 0)
{
bonus += CategoryMilestoneBonus *
profile.GetCategoryMasteredCount(entry.Category) /
categoryEntries.Count;
}
if (bonus > TotalBonusCap)
{
bonus = TotalBonusCap;
}
return bonus > 0;
}
[Usage("Bestiary")]
[Description("Opens the Hunter's Bestiary.")]
private static void OnBestiaryCommand(CommandEventArgs e)
{
if (e.Mobile is { Deleted: false } from)
{
HunterBestiaryGump.DisplayTo(from);
}
}
private static void OnWorldSave()
{
HunterProfileStore.Save(_profiles);
}
private static void OnWorldLoad()
{
ReloadProfiles();
HunterCombatTracker.Clear();
HunterDeathHook.ClearProcessedDeaths();
}
private static string _configSource =
"modernuo.json NOT FOUND - defaults in use";
private static readonly Dictionary<string, string> _rawSettings =
LoadRawSettings();
private static Dictionary<string, string> LoadRawSettings()
{
var dict = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase
);
var searched = new List<string>();
try
{
string path = null;
foreach (var candidate in ConfigCandidates(searched))
{
if (File.Exists(candidate))
{
path = candidate;
break;
}
}
if (path == null)
{
_configSource =
"modernuo.json NOT FOUND; searched: " +
string.Join(" | ", searched);
return dict;
}
_configSource = path;
using var doc = JsonDocument.Parse(
File.ReadAllText(path)
);
if (!doc.RootElement.TryGetProperty(
"settings",
out var settings
))
{
return dict;
}
foreach (var prop in settings.EnumerateObject())
{
dict[prop.Name] =
prop.Value.ValueKind == JsonValueKind.String
? prop.Value.GetString()
: prop.Value.ToString();
}
}
catch (Exception ex)
{
_configSource =
"modernuo.json read error: " + ex.Message;
}
return dict;
}
private static IEnumerable<string> ConfigCandidates(
List<string> searched
)
{
var roots = new List<string>();
foreach (var root in new[]
{
Core.BaseDirectory,
Directory.GetCurrentDirectory(),
AppContext.BaseDirectory
})
{
try
{
if (string.IsNullOrEmpty(root))
{
continue;
}
var full = Path.GetFullPath(root);
if (!roots.Contains(full))
{
roots.Add(full);
}
}
catch
{
// Ignore invalid path.
}
}
var seenDirs = new HashSet<string>(
StringComparer.OrdinalIgnoreCase
);
foreach (var root in roots)
{
var dir = root;
while (!string.IsNullOrEmpty(dir) &&
seenDirs.Add(dir))
{
searched.Add(dir);
foreach (var sub in new[]
{
"",
"Distribution",
"Configuration",
"Config",
"Configs",
"Server",
"Run"
})
{
yield return Path.Combine(
dir,
sub,
"modernuo.json"
);
}
dir = Directory.GetParent(dir)?.FullName;
}
}
}
public static bool GetBool(
string key,
bool defaultValue
) =>
_rawSettings.TryGetValue(key, out var s) &&
bool.TryParse(s, out var value)
? value
: defaultValue;
public static int GetInt(
string key,
int defaultValue
) =>
_rawSettings.TryGetValue(key, out var s) &&
int.TryParse(
s,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out var value
)
? value
: defaultValue;
public static double GetDouble(
string key,
double defaultValue
)
{
if (_rawSettings.TryGetValue(key, out var s))
{
if (double.TryParse(
s,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var value
) ||
double.TryParse(
s,
NumberStyles.Float,
CultureInfo.CurrentCulture,
out value
))
{
return value;
}
}
return defaultValue;
}
public static string GetString(
string key,
string defaultValue
) =>
_rawSettings.TryGetValue(key, out var value)
? value
: defaultValue;
public static TimeSpan GetTimeSpan(
string key,
TimeSpan defaultValue
) =>
_rawSettings.TryGetValue(key, out var s) &&
TimeSpan.TryParse(
s,
CultureInfo.InvariantCulture,
out var value
)
? value
: defaultValue;
public static TEnum GetEnum<TEnum>(
string key,
TEnum defaultValue
)
where TEnum : struct =>
_rawSettings.TryGetValue(key, out var s) &&
Enum.TryParse<TEnum>(
s,
true,
out var value
)
? value
: defaultValue;
public static void DebugLog(string message)
{
if (Debug)
{
_logger.Information("{Msg}", message);
}
}
[Usage("BestiaryDiag")]
[Description("Shows combat-event counters for bestiary hook diagnostics.")]
private static void OnBestiaryDiagCommand(CommandEventArgs e)
{
if (e.Mobile is not { } from)
{
return;
}
from.SendMessage(
$"AggressiveAction events seen: " +
$"{HunterCombatTracker.HitCount}; " +
$"tracked creatures: " +
$"{HunterCombatTracker.TrackedCount}."
);
from.SendMessage(
"Hit a mob 2-3 times in combat, then run this again. " +
"If the counter does not grow, no AggressiveAction event is received."
);
}
[Usage("BestiaryTest")]
[Description("Runs the treatise drop pipeline on the nearest registered creature.")]
private static void OnBestiaryTestCommand(CommandEventArgs e)
{
if (e.Mobile is not PlayerMobile from)
{
return;
}
BaseCreature target = null;
foreach (var bc in from.GetMobilesInRange<BaseCreature>(10))
{
if (!bc.Deleted &&
!bc.Summoned &&
!bc.IsBonded &&
bc.Hits > 0 &&
BestiaryRegistry.FindEntry(bc.GetType()) != null)
{
target = bc;
break;
}
}
if (target == null)
{
from.SendMessage(
"No registered creature within 10 tiles."
);
return;
}
var entry = BestiaryRegistry.FindEntry(target.GetType());
var effectiveChance = GlobalDropChance >= 0
? GlobalDropChance
: entry?.DropChance ?? 0;
from.SendMessage(
$"Running drop pipeline on {target.GetType().Name} " +
$"(chance {effectiveChance:P0})..."
);
var result = HunterDeathHook.OnCreatureDeath(
target,
from
);
HunterDeathHook.ClearProcessedDeath(target.Serial);
HunterCombatTracker.Forget(target.Serial);
if (result == null)
{
from.SendMessage(
0x44,
$"DROP EXECUTED at {effectiveChance:P0} chance. " +
"Check the corpse or the ground."
);
}
else
{
from.SendMessage(
0x22,
"NO DROP: " + result
);
}
DebugLog(
$"[Bestiary] TEST on {target.GetType().Name}: " +
$"{result ?? "DROP executed"}"
);
}
}