fix(ui): preserve retail item titles and spacing

Carry PublicWeenieDesc material type into the live object model so examination titles use the DAT-authored material prefix. Preserve retail AddItemInfo empty appends and embedded armor separator, restoring the deliberate blank rows between appraisal sections.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-24 05:21:01 +02:00
parent d78d09cfbc
commit 6718ee45a0
19 changed files with 341 additions and 46 deletions

View file

@ -649,11 +649,11 @@ public sealed class AppraisalUiController : IRetainedPanelController
: AppraisalView.Creature;
}
private static string BuildTitle(ClientObject obj, PropertyBundle properties)
private string BuildTitle(ClientObject obj, PropertyBundle properties)
{
string name = GetString(properties, DisplayedNameStringProperty);
if (string.IsNullOrWhiteSpace(name))
name = obj.GetAppropriateName();
name = _itemNames.ResolveAppropriateName(obj);
return obj.StackSize > 1
? $"{obj.StackSize.ToString(CultureInfo.CurrentCulture)} {name}"
: name;

View file

@ -87,6 +87,23 @@ internal sealed class ItemAppraisalReportBuilder
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
=> Add(value, sameParagraph: false, style);
/// <summary>
/// Preserve an empty retail <c>AddItemInfo("", ..., true)</c> append.
/// Once text exists this contributes one physical empty row; on an empty
/// glyph list retail has no preceding separator to append.
/// </summary>
public void BlankLine(
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
{
if (_fragments.Count == 0)
return;
_fragments.Add(new ItemAppraisalFragment(
string.Empty,
ItemAppraisalSeparator.Line,
style));
}
public ItemAppraisalReport Build()
=> _fragments.Count == 0
? ItemAppraisalReport.Empty

View file

@ -112,28 +112,33 @@ public static class ItemAppraisalTextFormatter
if (!string.IsNullOrWhiteSpace(imbuedBy))
report.Line($"Imbued by {imbuedBy}.");
if (!properties.Ints.TryGetValue(105u, out int workmanship))
return;
if (properties.Ints.TryGetValue(170u, out int salvagedItems)
&& salvagedItems > 0)
if (properties.Ints.TryGetValue(105u, out int workmanship))
{
double average = (double)workmanship / salvagedItems;
int workmanshipBand = Math.Clamp(
(int)Math.Round(average),
0,
10);
report.Line(
$"Workmanship: {WorkmanshipAdjective(workmanshipBand)} "
+ $"({average.ToString("0.00", CultureInfo.CurrentCulture)})");
report.Paragraph(
$"Salvaged from {salvagedItems.ToString(CultureInfo.CurrentCulture)} items.");
return;
if (properties.Ints.TryGetValue(170u, out int salvagedItems)
&& salvagedItems > 0)
{
double average = (double)workmanship / salvagedItems;
int workmanshipBand = Math.Clamp(
(int)Math.Round(average),
0,
10);
report.Line(
$"Workmanship: {WorkmanshipAdjective(workmanshipBand)} "
+ $"({average.ToString("0.00", CultureInfo.CurrentCulture)})");
report.Paragraph(
$"Salvaged from {salvagedItems.ToString(CultureInfo.CurrentCulture)} items.");
}
else
{
report.Line(
$"Workmanship: {WorkmanshipAdjective(Math.Clamp(workmanship, 0, 10))} "
+ $"({workmanship.ToString(CultureInfo.CurrentCulture)})");
}
}
report.Line(
$"Workmanship: {WorkmanshipAdjective(Math.Clamp(workmanship, 0, 10))} "
+ $"({workmanship.ToString(CultureInfo.CurrentCulture)})");
// Appraisal_ShowTinkeringInfo @ 0x004B1090 appends an empty
// AddItemInfo entry unconditionally, establishing the next section.
report.BlankLine();
}
/// <summary>
@ -144,11 +149,12 @@ public static class ItemAppraisalTextFormatter
RetailReportBuilder report,
PropertyBundle properties)
{
if (properties.Ints.TryGetValue(265u, out int setId)
&& EquipmentSetName(setId) is { } setName)
{
string? setName = properties.Ints.TryGetValue(265u, out int setId)
? EquipmentSetName(setId)
: null;
bool setShown = setName is not null;
if (setShown)
report.Line($"Set: {setName}");
}
(uint Property, string Label)[] ratingProperties =
[
@ -168,11 +174,23 @@ public static class ItemAppraisalTextFormatter
.Select(pair =>
$"{pair.Label} {properties.GetInt(pair.Property).ToString(CultureInfo.CurrentCulture)}")
.ToArray();
if (ratings.Length != 0)
bool ratingsShown = ratings.Length != 0;
if (ratingsShown)
report.Line($"Ratings: {string.Join(", ", ratings)}");
if (properties.GetInt(379u) is int vitality && vitality > 0)
{
report.Line(
$"This item adds {vitality.ToString(CultureInfo.CurrentCulture)} Vitality.");
ratingsShown = true;
}
// Appraisal_ShowRatings terminates its emitted block with an empty
// append; SetAppraiseInfo adds another when either Set or Ratings
// contributed content.
if (ratingsShown)
report.BlankLine();
if (setShown || ratingsShown)
report.BlankLine();
}
/// <summary>
@ -341,7 +359,9 @@ public static class ItemAppraisalTextFormatter
return;
}
report.Line(
// Retail's first armor string begins with a literal '\n' before
// AddItemInfo adds its ordinary separator.
report.Paragraph(
$"Armor Level: {armorLevel.ToString(CultureInfo.CurrentCulture)}",
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0001u));
ShowProtection(
@ -492,23 +512,34 @@ public static class ItemAppraisalTextFormatter
PropertyBundle properties,
RetailAppraisalNameResolver names)
{
// Appraisal_ShowSpecialProperties @ 0x004B0159.
report.BlankLine();
int carryLimit = properties.GetInt(279u);
if (carryLimit > 0)
{
report.BlankLine();
report.Line(
$"You can only carry "
+ $"{carryLimit.ToString("N0", CultureInfo.CurrentCulture)} "
+ "of these items.");
}
if (properties.Floats.TryGetValue(167u, out double cooldown)
&& cooldown > 0d)
{
report.Line($"Cooldown When Used: {FormatDeltaTime(cooldown)}");
if (properties.Ints.ContainsKey(280u))
report.BlankLine();
}
int cleave = properties.GetInt(292u);
if (cleave > 1)
{
report.Line(
$"Cleave: {cleave.ToString(CultureInfo.CurrentCulture)} enemies in front arc.");
report.BlankLine();
}
var special = new List<string>();
int slayer = properties.GetInt(166u);
@ -745,16 +776,23 @@ public static class ItemAppraisalTextFormatter
PropertyBundle properties)
{
int level = properties.GetInt(369u);
int skill = properties.GetInt(366u);
int difficulty = properties.GetInt(367u);
int specializedSkill = properties.GetInt(368u);
if (level > 0
|| (skill > 0 && difficulty > 0)
|| specializedSkill > 0)
{
report.BlankLine();
}
if (level > 0)
report.Line(
$"Use requires level {level.ToString(CultureInfo.CurrentCulture)}.");
int skill = properties.GetInt(366u);
int difficulty = properties.GetInt(367u);
if (skill > 0 && difficulty > 0)
report.Line(
$"Use requires {UsageSkillName(skill)} of at least "
+ $"{difficulty.ToString(CultureInfo.CurrentCulture)}.");
int specializedSkill = properties.GetInt(368u);
if (specializedSkill > 0)
report.Line(
$"Use requires specialized {UsageSkillName(specializedSkill)}.");
@ -794,11 +832,13 @@ public static class ItemAppraisalTextFormatter
report.Line(
$"Item XP: {experience.ToString("N0", CultureInfo.CurrentCulture)} / "
+ $"{nextExperience.ToString("N0", CultureInfo.CurrentCulture)}");
report.BlankLine();
if (properties.GetInt(352u) == 2)
{
report.Paragraph(
report.Line(
"This cloak has a chance to reduce an incoming attack by 200 damage.");
report.BlankLine();
}
}
@ -1776,6 +1816,10 @@ public static class ItemAppraisalTextFormatter
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
=> _report.Paragraph(value, style);
public void BlankLine(
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
=> _report.BlankLine(style);
public ItemAppraisalReport Build() => _report.Build();
}
}

View file

@ -1,4 +1,5 @@
using AcDream.Content;
using AcDream.Core.Items;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
@ -86,6 +87,33 @@ public sealed class RetailAppraisalNameResolver
? name
: string.Empty;
/// <summary>
/// Port of the material-decoration tail in
/// <c>ACCWeenieObject::GetObjectName @ 0x0058E6E0</c>. Retail first
/// chooses the singular/plural base name, removes an already-authored
/// occurrence of the resolved material, trims it, then prefixes the
/// material exactly once.
/// </summary>
public string ResolveAppropriateName(ClientObject obj)
{
ArgumentNullException.ThrowIfNull(obj);
string baseName = obj.GetAppropriateName();
if (obj.MaterialType is not { } materialType)
return baseName;
string material = ResolveMaterial(unchecked((int)materialType));
if (string.IsNullOrEmpty(material))
return baseName;
string remainder = baseName
.Replace(material, string.Empty, StringComparison.Ordinal)
.Trim();
return string.IsNullOrEmpty(remainder)
? material
: $"{material} {remainder}";
}
private static string Normalize(string value)
=> value.Replace('_', ' ');
}