acdream/tests/AcDream.App.Tests/UI/Layout/ItemAppraisalTextFormatterTests.cs
Erik d78d09cfbc feat(ui): port retail item appraisal reports
Follow ItemExamineUI's EoR dispatch and wording for item assessment instead of the generic projection. Resolve material and creature names through installed DAT maps, cover specialized item branches and item-XP curves, and narrow AP-110 to the remaining live/localized preview seams.
2026-07-23 19:08:05 +02:00

802 lines
28 KiB
C#

using System.Numerics;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Spells;
namespace AcDream.App.Tests.UI.Layout;
public sealed class ItemAppraisalTextFormatterTests
{
[Fact]
public void WeaponAndMagic_AreProjectedInRetailOrderWithDatDescriptions()
{
var obj = new ClientObject
{
ObjectId = 0x50000001u,
Name = "Flaming Sword",
Type = ItemType.MeleeWeapon,
ValidLocations = EquipMask.MeleeWeapon,
Value = 12_500,
Burden = 450,
Workmanship = 8.4f,
};
var properties = new PropertyBundle();
properties.Ints[353u] = 2;
properties.Ints[19u] = 12_500;
properties.Ints[5u] = 450;
properties.Ints[204u] = 4;
properties.Ints[105u] = 8;
properties.Ints[106u] = 300;
properties.Ints[107u] = 250;
properties.Ints[108u] = 500;
properties.Ints[117u] = 20;
properties.Floats[29u] = 1.05d;
properties.Strings[14u] = "Use this item to attack.";
properties.Strings[16u] = "A finely balanced enchanted blade.";
SpellMetadata flame = Spell(101u, "Flame Bolt VI", "Hurls a bolt of flame.");
SpellMetadata armor = Spell(102u, "Impenetrability VI", "Increases the target's armor.");
AppraiseInfoParser.Parsed appraisal = Parsed(
properties,
spells: [101u, 0x8000_0066u],
weapon: new AppraiseInfoParser.WeaponProfile(
DamageType: 1u,
WeaponTime: 30u,
WeaponSkill: 44u,
Damage: 40u,
DamageVariance: 0.25d,
DamageMod: 1d,
WeaponLength: 1d,
MaxVelocity: 0d,
WeaponOffense: 1.15d,
MaxVelocityEstimated: 0u));
string report = ItemAppraisalTextFormatter.Build(
obj,
appraisal,
id => id switch
{
101u => flame,
102u => armor,
_ => null,
});
Assert.Contains("Value: 12,500", report);
Assert.Contains("Workmanship: Utterly flawless (8)", report);
Assert.Contains("Skill: Heavy Weapons (Sword)", report);
Assert.Contains("Damage: 30 - 40, Slashing", report);
Assert.Contains("Elemental Damage Bonus: 4, Slashing.", report);
Assert.Contains("Speed: Fast (30)", report);
Assert.Contains("Bonus to Attack Skill: +15%.", report);
Assert.Contains("Bonus to Melee Defense: +5%.", report);
Assert.Contains("Spells: Flame Bolt VI", report);
Assert.DoesNotContain("Spells: Flame Bolt VI, Impenetrability VI", report);
Assert.Contains("Spellcraft: 300.", report);
Assert.Contains("Mana: 250 / 500.", report);
Assert.Contains("Mana Cost: 20.", report);
Assert.Contains("Spell Descriptions:", report);
Assert.Contains("~ Flame Bolt VI: Hurls a bolt of flame.", report);
Assert.Contains("Enchantments:", report);
Assert.Contains(
"~ Impenetrability VI: Increases the target's armor.",
report);
Assert.True(
report.IndexOf("Skill:", StringComparison.Ordinal)
< report.IndexOf("Spells:", StringComparison.Ordinal));
Assert.True(
report.IndexOf("Spells:", StringComparison.Ordinal)
< report.IndexOf("Spell Descriptions:", StringComparison.Ordinal));
Assert.EndsWith("A finely balanced enchanted blade.", report);
}
[Fact]
public void NpcLooksLikeObjectCreature_UsesRetailUnknownsAndSuppressesNegativeCapacity()
{
var obj = new ClientObject
{
ObjectId = 0x50000005u,
Name = "Black Phyntos Hive",
Type = ItemType.Creature,
ItemsCapacity = -1,
ContainersCapacity = -1,
};
var properties = new PropertyBundle();
properties.Strings[16u] =
"A hollowed out tree trunk that has a Phyntos Wasp Hive in it.";
ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport(
obj,
Parsed(properties),
_ => null);
Assert.Equal(
"Value: ???\nBurden: Unknown\n\n"
+ "A hollowed out tree trunk that has a Phyntos Wasp Hive in it.",
report.ToString());
Assert.DoesNotContain("Can hold", report.ToString());
Assert.Collection(
report.Fragments,
value =>
{
Assert.Equal(ItemAppraisalSeparator.None, value.Separator);
Assert.Equal("Value: ???", value.Text);
},
burden =>
{
Assert.Equal(ItemAppraisalSeparator.Line, burden.Separator);
Assert.Equal("Burden: Unknown", burden.Text);
},
description =>
{
Assert.Equal(ItemAppraisalSeparator.Paragraph, description.Separator);
Assert.Equal(properties.Strings[16u], description.Text);
});
}
[Fact]
public void HookWithHookProfile_SuppressesHooksOwnCapacity()
{
var obj = new ClientObject
{
ObjectId = 0x50000015u,
Name = "Weapon Hook",
Type = ItemType.Container,
ItemsCapacity = 24,
ContainersCapacity = 1,
HookItemTypes = (uint)ItemType.Weapon,
HookType = 4u,
};
ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport(
obj,
Parsed(
new PropertyBundle(),
hook: new AppraiseInfoParser.HookProfile(
Flags: 0u,
ValidLocations: 0u,
AmmoType: 0u)),
_ => null);
Assert.DoesNotContain("Can hold", report.ToString());
}
[Fact]
public void OrdinaryContainer_PresentsCapacityAsRetailParagraph()
{
var obj = new ClientObject
{
ObjectId = 0x50000006u,
Name = "Backpack",
Type = ItemType.Container,
ItemsCapacity = 24,
ContainersCapacity = 1,
};
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(new PropertyBundle()),
_ => null);
Assert.Equal(
"Value: ???\nBurden: Unknown\n\n"
+ "Can hold up to 24 items and 1 containers.",
report);
}
[Fact]
public void BookCapacity_UsesRetailUsedThenTotalPropertyOrder()
{
var obj = new ClientObject
{
ObjectId = 0x5000000Bu,
Name = "Book",
Type = ItemType.Writable,
};
var properties = new PropertyBundle();
properties.Ints[174u] = 3;
properties.Ints[175u] = 10;
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(properties),
_ => null);
Assert.EndsWith("3 of 10 pages full.", report);
}
[Theory]
[InlineData((ushort)0x0000, ItemAppraisalFontStyle.Detrimental)]
[InlineData((ushort)0x0008, ItemAppraisalFontStyle.Beneficial)]
public void WeaponDamage_UsesRetailEnchantmentFontSelection(
ushort highHalf,
ItemAppraisalFontStyle expected)
{
var obj = new ClientObject
{
ObjectId = 0x50000007u,
Name = "Sword",
Type = ItemType.MeleeWeapon,
ValidLocations = EquipMask.MeleeWeapon,
};
AppraiseInfoParser.Parsed parsed = Parsed(
new PropertyBundle(),
weapon: new AppraiseInfoParser.WeaponProfile(
DamageType: 1u,
WeaponTime: 30u,
WeaponSkill: 44u,
Damage: 40u,
DamageVariance: 0d,
DamageMod: 1d,
WeaponLength: 1d,
MaxVelocity: 0d,
WeaponOffense: 1d,
MaxVelocityEstimated: 0u),
weaponEnchantments: ((ushort)0x0008, highHalf));
ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport(
obj,
parsed,
_ => null);
ItemAppraisalFragment damage = Assert.Single(
report.Fragments,
fragment => fragment.Text.StartsWith(
"Damage:",
StringComparison.Ordinal));
Assert.Equal(expected, damage.Style);
}
[Fact]
public void TextLayout_PreservesRetailPaletteAcrossParagraphs()
{
Vector4 normal = Vector4.One;
var beneficial = new Vector4(0f, 1f, 0f, 1f);
var detrimental = new Vector4(1f, 0f, 0f, 1f);
var text = new UiText
{
Width = 1_000f,
FontColorPalette = [normal, beneficial, detrimental],
};
var report = new ItemAppraisalReport(
[
new ItemAppraisalFragment(
"Normal",
ItemAppraisalSeparator.None,
ItemAppraisalFontStyle.Normal),
new ItemAppraisalFragment(
"High",
ItemAppraisalSeparator.Line,
ItemAppraisalFontStyle.Beneficial),
new ItemAppraisalFragment(
"Low",
ItemAppraisalSeparator.Paragraph,
ItemAppraisalFontStyle.Detrimental),
]);
IReadOnlyList<UiText.Line> lines = ItemAppraisalTextLayout.Shape(
text,
report);
Assert.Equal(["Normal", "High", "", "Low"], lines.Select(line => line.Text));
Assert.Equal(normal, lines[0].Color);
Assert.Equal(beneficial, lines[1].Color);
Assert.Equal(detrimental, lines[3].Color);
}
[Fact]
public void LockInformation_UsesRetailPresenceRulesWordingAndParagraphs()
{
var obj = new ClientObject
{
ObjectId = 0x50000009u,
Name = "Locked Chest",
Type = ItemType.Container,
};
var properties = new PropertyBundle();
properties.Bools[3u] = true;
properties.Ints[38u] = 212;
properties.Ints[173u] = 42;
ItemAppraisalReport report = ItemAppraisalTextFormatter.BuildReport(
obj,
Parsed(properties),
_ => null);
Assert.Contains(
report.Fragments,
fragment => fragment.Text == "Locked"
&& fragment.Separator == ItemAppraisalSeparator.Paragraph);
Assert.Contains(
report.Fragments,
fragment => fragment.Text
== "The lock looks difficult to pick (Resistance 212)."
&& fragment.Separator == ItemAppraisalSeparator.Paragraph);
}
[Fact]
public void MissingLockedFlag_ReportsLockpickBonusInsteadOfInventingLockState()
{
var obj = new ClientObject
{
ObjectId = 0x5000000Au,
Name = "Lockpick",
Type = ItemType.Misc,
};
var properties = new PropertyBundle();
properties.Ints[38u] = -7;
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(properties),
_ => null);
Assert.Contains("Bonus to Lockpick Skill: -7", report);
Assert.DoesNotContain("Locked", report);
Assert.DoesNotContain("Unlocked", report);
}
[Fact]
public void ArmorRequirementsAndUses_UseRetailLabelsAndResistanceBands()
{
var obj = new ClientObject
{
ObjectId = 0x50000002u,
Name = "Platemail Hauberk",
Type = ItemType.Armor,
ValidLocations = EquipMask.ChestArmor,
Structure = 8,
MaxStructure = 10,
};
var properties = new PropertyBundle();
properties.Ints[28u] = 200;
properties.Ints[158u] = 1;
properties.Ints[159u] = 6;
properties.Ints[160u] = 250;
properties.Ints[324u] = 4;
properties.Ints[369u] = 50;
properties.Ints[370u] = 3;
properties.Ints[92u] = 8;
properties.Bools[69u] = false;
properties.Strings[25u] = "Tester";
AppraiseInfoParser.Parsed appraisal = Parsed(
properties,
armor: new AppraiseInfoParser.ArmorProfile(
SlashingProtection: 1.2f,
PiercingProtection: 0.8f,
BludgeoningProtection: 0.4f,
ColdProtection: 0f,
FireProtection: 1.6f,
AcidProtection: 2f,
NetherProtection: 1f,
LightningProtection: 0.2f));
string report = ItemAppraisalTextFormatter.Build(
obj,
appraisal,
_ => null);
Assert.Contains("Ratings: Dam 3", report);
Assert.Contains("Armor Level: 200", report);
Assert.Contains("Slashing: Above Average (240)", report);
Assert.Contains("Piercing: Average (160)", report);
Assert.Contains("Bludgeoning: Below Average (80)", report);
Assert.Contains("Cold: None (0)", report);
Assert.Contains("Fire: Excellent (320)", report);
Assert.Contains("Acid: Unparalleled (400)", report);
Assert.Contains("Electric: Poor (40)", report);
Assert.Contains("Wield requires Viamontian", report);
Assert.Contains("Wield requires Melee Defense 250", report);
Assert.Contains("Use requires level 50.", report);
Assert.Contains("Number of uses remaining: 8", report);
Assert.Contains("Created by Tester.", report);
Assert.Contains("This item cannot be sold.", report);
}
[Fact]
public void Launcher_UsesDamageBonusModifierAndAmmunitionWording()
{
var obj = new ClientObject
{
ObjectId = 0x50000003u,
Name = "Longbow",
Type = ItemType.MissileWeapon,
ValidLocations = EquipMask.MissileWeapon,
AmmoType = 1,
};
AppraiseInfoParser.Parsed appraisal = Parsed(
new PropertyBundle(),
weapon: new AppraiseInfoParser.WeaponProfile(
DamageType: 2u,
WeaponTime: 45u,
WeaponSkill: 47u,
Damage: 12u,
DamageVariance: 0d,
DamageMod: 1.13d,
WeaponLength: 1d,
MaxVelocity: 50d,
WeaponOffense: 1d,
MaxVelocityEstimated: 1u));
string report = ItemAppraisalTextFormatter.Build(
obj,
appraisal,
_ => null);
Assert.Contains("Skill: Missile Weapons", report);
Assert.Contains("Damage Bonus: 12", report);
Assert.DoesNotContain("Damage Bonus: 12, Piercing", report);
Assert.Contains("Damage Modifier: +13%.", report);
Assert.Contains("Speed: Average (45)", report);
Assert.Contains("Range: 85 yds. (based on STRENGTH 100)", report);
Assert.Contains("Uses arrows as ammunition.", report);
}
[Fact]
public void SpecialProperties_AreCombinedUsingRetailNames()
{
var obj = new ClientObject
{
ObjectId = 0x50000004u,
Name = "Imbued Katar",
Type = ItemType.MeleeWeapon,
};
var properties = new PropertyBundle();
properties.Ints[279u] = 3;
properties.Ints[292u] = 4;
properties.Ints[179u] = 0x0000_0481;
properties.Ints[114u] = 1;
properties.Ints[33u] = 1;
properties.Floats[167u] = 90d;
properties.Bools[99u] = true;
properties.Bools[100u] = true;
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(properties),
_ => null);
Assert.Contains("You can only carry 3 of these items.", report);
Assert.Contains("Cooldown When Used: 1m 30s ", report);
Assert.Contains("Cleave: 4 enemies in front arc.", report);
Assert.Contains(
"Properties: Critical Strike, Cold Rending, +1 Melee Defense, Attuned, Bonded, Ivoryable, Dyeable",
report);
Assert.Contains("This item cannot be further imbued.", report);
}
[Fact]
public void SpecializedItemBranches_FollowRetailNamesWordingAndOrder()
{
var obj = new ClientObject
{
ObjectId = 0x50000020u,
Name = "Ruby Coat",
Type = ItemType.Clothing,
ValidLocations = EquipMask.ChestWear,
Priority = 0x0008u | 0x0010u | 0x0020u | 0x0040u,
};
var properties = new PropertyBundle();
properties.Ints[171u] = 2;
properties.Strings[39u] = "Alyssa";
properties.Strings[40u] = "Borelean";
properties.Ints[105u] = 10;
properties.Ints[170u] = 4;
properties.Ints[265u] = 13;
properties.Ints[370u] = 3;
properties.Ints[371u] = -4;
properties.Ints[379u] = 2;
properties.Ints[86u] = 50;
properties.Ints[87u] = 50;
properties.Int64s[4u] = 300;
properties.Int64s[5u] = 100;
properties.Ints[319u] = 10;
properties.Ints[320u] = 3;
properties.Ints[109u] = 250;
properties.Ints[188u] = 4;
properties.Bools[108u] = true;
properties.Ints[17u] = 42;
properties.Ints[267u] = 1;
properties.Ints[98u] = 1;
properties.Ints[268u] = 90_061;
properties.Strings[16u] = "Ruby sword";
properties.Ints[172u] = 5;
properties.Ints[131u] = 0x26;
properties.Ints[177u] = 2;
properties.Ints[178u] = 0x26;
properties.Ints[111u] = 2 | 0x20 | 0x10;
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(properties),
_ => null,
Names(
materials: new Dictionary<uint, string> { [0x26u] = "Ruby" }));
Assert.Contains("This item has been tinkered 2 times.", report);
Assert.Contains("Last tinkered by Alyssa.", report);
Assert.Contains("Imbued by Borelean.", report);
Assert.Contains("Workmanship: Well-crafted (2.50)", report);
Assert.Contains("Salvaged from 4 items.", report);
Assert.Contains("Set: Soldier's", report);
Assert.Contains("Ratings: Dam 3", report);
Assert.DoesNotContain("Dam Resist", report);
Assert.Contains("This item adds 2 Vitality.", report);
Assert.Contains(
"Covers Chest, Abdomen, Upper Arms, Lower Arms",
report);
Assert.Contains("Restricted to characters of Level 50.", report);
Assert.Contains("Item Level: 3 / 10", report);
Assert.Contains("Item XP: 300 / 600", report);
Assert.Contains("Activation requires Arcane Lore: 250, Viamontian", report);
Assert.Contains(
"This rare item has a timer restriction of 3 minutes. "
+ "You will not be able to use another rare item with a timer "
+ "within 3 minutes of using this one.",
report);
Assert.Contains("Rare #42", report);
Assert.Contains(
"This item expires in 1 days, 1 hours, 1 minutes, 1 seconds.",
report);
Assert.Contains("Priceless Ruby sword, set with 2 Rubies", report);
Assert.Contains("Player Killers may not use this portal.", report);
Assert.Contains("This portal cannot be recalled nor linked to.", report);
Assert.Contains("This portal cannot be summoned.", report);
Assert.True(
report.IndexOf("Set: Soldier's", StringComparison.Ordinal)
< report.IndexOf("Covers Chest", StringComparison.Ordinal));
Assert.True(
report.IndexOf("Rare #42", StringComparison.Ordinal)
< report.IndexOf("This item expires", StringComparison.Ordinal));
}
[Fact]
public void SlayerAndWieldCreatureRequirements_UseRetailEnumNames()
{
var properties = new PropertyBundle();
properties.Ints[166u] = 10;
properties.Ints[158u] = 11;
properties.Ints[159u] = 0;
properties.Ints[160u] = 10;
string report = ItemAppraisalTextFormatter.Build(
new ClientObject
{
ObjectId = 0x50000021u,
Name = "Drudge Slayer",
Type = ItemType.MeleeWeapon,
},
Parsed(properties),
_ => null,
Names(creatures: new Dictionary<uint, string> { [10u] = "Drudge" }));
Assert.Contains("Properties: Drudge slayer", report);
Assert.Contains("Wield requires Drudge type", report);
Assert.DoesNotContain("Creature type 10", report);
Assert.DoesNotContain("creature 10", report);
}
[Fact]
public void HealerProfile_UsesHealingRowsInsteadOfConsumableBoostProse()
{
var properties = new PropertyBundle();
properties.Ints[89u] = 2;
properties.Ints[90u] = 12;
properties.Floats[100u] = 1.25d;
var obj = new ClientObject
{
ObjectId = 0x50000022u,
Name = "Healing Kit",
Type = ItemType.Misc,
PublicWeenieBitfield = (uint)PublicWeenieFlags.Healer,
};
string report = ItemAppraisalTextFormatter.Build(
obj,
Parsed(properties),
_ => null);
Assert.Contains("Bonus to Healing Skill: 12", report);
Assert.Contains("Restoration Bonus: 125%", report);
Assert.DoesNotContain("Restores 12 Health when used.", report);
}
[Fact]
public void FailedMagicAndLauncher_PreserveRetailUnknownRows()
{
var obj = new ClientObject
{
ObjectId = 0x50000023u,
Name = "Unknown Bow",
Type = ItemType.MissileWeapon,
ValidLocations = EquipMask.MissileWeapon,
AmmoType = 1,
};
AppraiseInfoParser.Parsed appraisal = Parsed(
new PropertyBundle(),
spells: [101u],
weapon: new AppraiseInfoParser.WeaponProfile(
DamageType: 2u,
WeaponTime: uint.MaxValue,
WeaponSkill: 47u,
Damage: uint.MaxValue,
DamageVariance: 0d,
DamageMod: 1d,
WeaponLength: 1d,
MaxVelocity: 0d,
WeaponOffense: 1d,
MaxVelocityEstimated: 0u),
success: false);
string report = ItemAppraisalTextFormatter.Build(
obj,
appraisal,
_ => null);
Assert.Equal(2, Count(report, "Spells: unknown."));
Assert.Contains("Damage Bonus: Unknown", report);
Assert.Contains("Damage Modifier: Unknown", report);
Assert.Contains("Speed: Unknown", report);
Assert.Contains("Range: Unknown", report);
}
[Fact]
public void OrdinaryStaminaConsumable_UsesRetailConsumedWording()
{
var properties = new PropertyBundle();
properties.Ints[89u] = 4;
properties.Ints[90u] = -20;
string report = ItemAppraisalTextFormatter.Build(
new ClientObject
{
ObjectId = 0x50000024u,
Name = "Stamina Food",
Type = ItemType.Food,
},
Parsed(properties),
_ => null);
Assert.Contains("Depletes 20 Stamina when consumed.", report);
}
[Theory]
[InlineData(1, 250L, "Item Level: 3 / 10", "Item XP: 250 / 300")]
[InlineData(2, 300L, "Item Level: 3 / 10", "Item XP: 300 / 700")]
public void ItemExperienceStyles_UseRetailCumulativeCurves(
int style,
long experience,
string expectedLevel,
string expectedExperience)
{
var properties = new PropertyBundle();
properties.Int64s[4u] = experience;
properties.Int64s[5u] = 100L;
properties.Ints[319u] = 10;
properties.Ints[320u] = style;
string report = ItemAppraisalTextFormatter.Build(
new ClientObject
{
ObjectId = 0x50000025u,
Name = "Leveling Item",
Type = ItemType.Misc,
},
Parsed(properties),
_ => null);
Assert.Contains(expectedLevel, report);
Assert.Contains(expectedExperience, report);
}
[Fact]
public void EndOfRetailDatNameResolver_LoadsMaterialAndCreatureNames()
{
string? datDirectory = ResolveDatDirectory();
if (datDirectory is null)
return;
using var dats =
new AcDream.App.Tests.BoundedTestDatCollection(datDirectory);
CreatureDisplayNameResolver creatures =
CreatureDisplayNameResolver.Load(dats);
RetailAppraisalNameResolver names =
RetailAppraisalNameResolver.Load(dats, creatures);
Assert.Equal("Ruby", names.ResolveMaterial(0x26));
Assert.Equal("Ghost", names.ResolveCreature(77));
}
private static AppraiseInfoParser.Parsed Parsed(
PropertyBundle properties,
uint[]? spells = null,
AppraiseInfoParser.WeaponProfile? weapon = null,
AppraiseInfoParser.ArmorProfile? armor = null,
AppraiseInfoParser.HookProfile? hook = null,
(ushort Highlight, ushort Color)? weaponEnchantments = null,
bool success = true)
=> new(
Guid: 0x50000001u,
Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable,
Success: success,
Properties: properties,
SpellBook: spells ?? [],
ArmorProfile: armor,
CreatureProfile: null,
WeaponProfile: weapon,
HookProfile: hook,
ArmorLevels: null,
ArmorEnchantments: null,
WeaponEnchantments: weaponEnchantments,
ResistEnchantments: null);
private static SpellMetadata Spell(
uint id,
string name,
string description)
=> new(
SpellId: id,
Name: name,
School: "War Magic",
Family: id,
IconId: 0u,
SpellWords: string.Empty,
Duration: 0f,
ManaCost: 10,
IsDebuff: false,
IsFellowship: false,
Description: description,
SortKey: 0,
Difficulty: 0,
Flags: 0u,
Generation: 6,
IsFastWindup: false,
IsOffensive: true,
IsUntargeted: false,
Speed: 0f,
CasterEffect: 0u,
TargetEffect: 0u,
TargetMask: 0u,
SpellType: 0);
private static RetailAppraisalNameResolver Names(
IReadOnlyDictionary<uint, string>? materials = null,
IReadOnlyDictionary<uint, string>? creatures = null)
=> new(
materials ?? new Dictionary<uint, string>(),
new CreatureDisplayNameResolver(
creatures ?? new Dictionary<uint, string>()));
private static int Count(string source, string value)
{
int count = 0;
int start = 0;
while ((start = source.IndexOf(
value,
start,
StringComparison.Ordinal)) >= 0)
{
count++;
start += value.Length;
}
return count;
}
private static string? ResolveDatDirectory()
{
string? configured =
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(configured)
&& Directory.Exists(configured))
{
return configured;
}
string fallback = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
return Directory.Exists(fallback) ? fallback : null;
}
}