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.
This commit is contained in:
parent
2c00d53db2
commit
d78d09cfbc
12 changed files with 1258 additions and 193 deletions
|
|
@ -123,6 +123,15 @@ bytes into `255/255`. CreateObject now sign-extends both capacity bytes, so
|
|||
`Appraisal_ShowCapacity`'s existing positive tests naturally suppress the
|
||||
sentinels without a name/type workaround.
|
||||
|
||||
The subsequent exhaustive item-report pass ports the remaining EoR formatter
|
||||
branches: literal equipment-set names, positive-only ratings, salvage
|
||||
workmanship, clothing coverage, failed weapon unknowns, exact item-XP curves,
|
||||
activation/healer/rare/magic prose, and decorated lifespan/material/gem/portal
|
||||
descriptions. Installed DAT maps now supply material and creature/slayer names.
|
||||
AP-110 retains only item preview, live player/localization projections,
|
||||
character detail, and creature FontInfo state; the connected item visual is
|
||||
the remaining acceptance gate.
|
||||
|
||||
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
||||
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
||||
`src/AcDream.App/UI/Layout/AppraisalUiController.cs`;
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
|
||||
| AP-108 | Paperdoll/AutoWield still omit the remaining missile/held restrictions and corrupt-mask branch of full `AutoWieldIsLegal`, dual-wield/off-hand rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **AutoWear legality retired from this row 2026-07-23:** inventory activation and paperdoll drops now apply the retail clothing-priority/location blocker lookup and exact `"You must remove your %s to wear that"` system notice. **Primary replacement retired 2026-07-14; Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, live doll, AutoWear conflict reporting, and primary weapon/incompatible shield/mismatched ammo blocker sequencing work in peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60`; `CPlayerSystem::AutoWearIsLegal @ 0x0055EF40`; `CPlayerSystem::AutoWield @ 0x00560A60`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
|
||||
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
||||
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, and retail-ordered item reports covering weapon/armor/caster modifiers, spell names plus DAT descriptions, requirements, capacities, cooldown/special properties, uses, crafting, rare, and description data. Item reports also preserve appraisal-only unknowns, hook/capacity/lock presence rules, paragraph boundaries, and the authored normal/beneficial/detrimental color list. It still lacks item-object preview, player-dependent effective shield projection, DAT display-name resolution for equipment sets and slayer creature types, some expiry/material/portal/PK-specialized description branches, exhaustive character detail regions, and exact creature appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the remaining examination mechanisms require player-state or DAT display-name resolvers and object-preview ownership rather than fabricated content | Uncommon item/character assessments may omit the listed specialized branches and item-object presentation; unresolved equipment-set/slayer values use their numeric identity; enchanted/incomplete creature appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::AddItemInfo @ 0x004AC050`; `ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680`; `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`; `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`; `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
|
||||
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination has its independent authored floaty layout, inscription transaction, retail creature stat/rating/animated-preview presentation, default selection-follow, and the full EoR item-report dispatch: appraisal-only unknowns; exact equipment-set/rating/tinkering/weapon/armor/caster/requirement/XP/healer/rare prose; ordinary/enchantment DAT spell descriptions; DAT material and creature names; expiry, decorated material/gem descriptions; and portal/PK restrictions with authored item colors. It still lacks item-object preview, player-dependent effective shield projection, live cooldown-remaining projection, localized augmentation-cost `StringInfo`, exhaustive character detail regions, and exact creature appraisal FontInfo-list selection. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs`; `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs`; `src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs`; `src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation/inscription/creature-preview/item-report loop cover the active loops; the residual examination mechanisms require live player/enchantment/localization state or object-preview ownership rather than fabricated content | Item assessments omit only the listed live/localized/preview projections; enchanted/incomplete creature appraisal rows use the normal authored font until the exact FontInfo list is bound; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `BasicCreatureExamineUI::Init @ 0x004AB9C0`; `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`; `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`; `AttributeInfoRegion::Update @ 0x004F1D90`; `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::AddItemInfo @ 0x004AC050`; `ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680`; `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`; `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`; `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`; `ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990`; `MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
|
||||
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
|
||||
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
|
||||
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ bodies begin: spell-bar overflow controls, status-bar Use/Assess commands,
|
|||
assessment information, equipped-child picking, vendor browsing, and
|
||||
server-authoritative vendor transactions. Slice 1's DAT-authored arrow-only
|
||||
favorite-spell scrollbar, fixed 18-cell capacity, authored arrow sides/media,
|
||||
and persistent manual offset passed the connected gate. Slice 2 is active: the
|
||||
status hand follows canonical selection/object notices, ghosts empty and
|
||||
and persistent manual offset passed the connected gate. Slice 2's status hand
|
||||
follows canonical selection/object notices, ghosts empty and
|
||||
explicitly unusable selections, and routes enabled weapons, direct-use items,
|
||||
and use-on-target tools through the existing interaction transaction and
|
||||
cursor owners. Its connected state gate passed. A named-retail correction now
|
||||
|
|
@ -38,8 +38,11 @@ shared main-panel content; and inscription was display-only. Exact flag
|
|||
decoding now reaches the balanced request owner, retail's
|
||||
`gmFloatyExaminationUI` has an independent top-level lifetime, and the
|
||||
imported multiline field sends the exact `SetInscription (0x00BF)`
|
||||
transaction under retail ownership/authorship rules. The corrective connected
|
||||
gate is next.
|
||||
transaction under retail ownership/authorship rules; those corrections are
|
||||
user-confirmed. The item view now follows the complete EoR formatter dispatch,
|
||||
including literal set/rating/tinkering prose, weapon/armor/magic/XP/
|
||||
activation/healer/rare branches, and installed-DAT material and creature
|
||||
names. Its connected item-report comparison is Slice 3's remaining gate.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ transactions. This is deliberately using the extracted interaction owners and
|
|||
canonical shared main-panel host before quest/emote/character-creation bodies
|
||||
broaden the feature surface.
|
||||
|
||||
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2 is
|
||||
active: the status hand's selected-object availability and existing
|
||||
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2's
|
||||
status hand selected-object availability and existing
|
||||
Use/AutoWield/use-on-target command routing passed their connected gate.
|
||||
The follow-up retail correction for zero-valued direct-use items, named
|
||||
AutoWear blockers, and carried-use delivery is user-confirmed. The requested
|
||||
|
|
@ -104,8 +104,11 @@ appraisal busy reference held, retail's examination UI had been mounted as a
|
|||
shared main-panel page rather than its own floaty window, and inscription was
|
||||
display-only. Exact response flags, the independent
|
||||
`gmFloatyExaminationUI` lifetime, and the retail `SetInscription (0x00BF)`
|
||||
edit transaction are now ported. The corrective connected
|
||||
examination/inscription gate is next.
|
||||
edit transaction are now ported and user-confirmed. Its item path now follows
|
||||
the complete EoR formatter dispatch, including literal set/rating/tinkering
|
||||
prose, weapon/armor/magic/XP/activation/healer/rare branches, and installed-DAT
|
||||
material/creature names. The connected item-report comparison is Slice 3's
|
||||
remaining gate.
|
||||
|
||||
Carried:
|
||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||
|
|
|
|||
|
|
@ -196,6 +196,23 @@ skips and 7,942 complete-solution tests / 5 skips pass while the previously
|
|||
launched client retains the normal Release output lock. The corrected
|
||||
connected item visual remains the closeout gate.
|
||||
|
||||
The exhaustive item-report correction replaces the remaining generic/numeric
|
||||
approximations with the complete named-retail dispatch. Equipment sets use the
|
||||
literal EoR table; ratings, tinkering/salvage averages, coverage, failed weapon
|
||||
unknowns, level restrictions, all three item-XP curves, activation heritage,
|
||||
healer/ordinary boost behavior, rare timers, and magic `~ Name: Description`
|
||||
rows now preserve retail wording and ordering. Description construction ports
|
||||
lifespan prose, workmanship/material/gem decoration and portal/PK restriction
|
||||
bits. `RetailAppraisalNameResolver` follows retail's master
|
||||
`EnumIDMap -> sub-enum 1 DualEnumIDMap` material chain and shares the installed
|
||||
creature mapper for slayers and wield requirements; a production-DAT test pins
|
||||
Ruby and Ghost. AP-110 is narrowed to item preview and the projections that
|
||||
need live player/localization state (effective shield, cooldown remaining, and
|
||||
augmentation-cost `StringInfo`), plus character and creature-font residuals.
|
||||
The focused item formatter suite passes 21/21, App Release passes 3,531 tests /
|
||||
3 skips, and the complete Release solution passes 7,952 tests / 5 skips. The
|
||||
connected item-report comparison is the remaining Slice 3 gate.
|
||||
|
||||
## Slice 1 — spell-bar overflow arrows
|
||||
|
||||
### Retail oracle
|
||||
|
|
|
|||
|
|
@ -517,6 +517,139 @@ ShowRemainingItemFields(profile):
|
|||
short and long descriptions
|
||||
```
|
||||
|
||||
### Exhaustive EoR item-report branches
|
||||
|
||||
The common projection above was re-audited against every helper called by
|
||||
`ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`. The remaining item branches are
|
||||
not generic property labels; retail applies the following exact rules.
|
||||
|
||||
```text
|
||||
ShowTinkering:
|
||||
if NumTimesTinkered (171):
|
||||
"This item has been tinkered %d time[s]."
|
||||
if TinkerName (39): "Last tinkered by %s."
|
||||
if ImbuerName (40): "Imbued by %s."
|
||||
if Workmanship (105):
|
||||
if NumItemsInMaterial (170) is absent:
|
||||
"Workmanship: <adjective> (<integer workmanship>)"
|
||||
else:
|
||||
average = workmanship / NumItemsInMaterial
|
||||
"Workmanship: <adjective of rounded average> (<average, two decimals>)"
|
||||
blank line
|
||||
"Salvaged from %d items."
|
||||
|
||||
ShowSet:
|
||||
EquipmentSetId (265) selects the literal EoR set-name table.
|
||||
Unknown and explicitly unused entries produce no row.
|
||||
Known entries produce "Set: <name>".
|
||||
|
||||
ShowRatings:
|
||||
append only positive ratings, in this order:
|
||||
Dam, Dam Resist, Crit, Crit Dam, Crit Resist, Crit Dam Resist,
|
||||
Heal Boost, Nether Resist, Life Resist
|
||||
render as "Ratings: Dam 3, Crit 2" (there is no plus sign)
|
||||
positive Vitality (379) is a separate
|
||||
"This item adds %d Vitality."
|
||||
|
||||
ShowWeaponAndArmorData:
|
||||
only interpret WeaponProfile inside weapon/shield valid-location bits
|
||||
preserve retail Unknown sentinels for damage, speed, and range
|
||||
for ordinary clothing append:
|
||||
"Covers Head, Chest, Abdomen, Upper Arms, Lower Arms, Hands,
|
||||
Upper Legs, Lower Legs, Feet"
|
||||
selecting the names from ClothingPriority bits in that exact order
|
||||
|
||||
ShowShortMagic:
|
||||
list only ordinary spell ids (high bit clear)
|
||||
a failed appraisal prints "Spells: unknown."
|
||||
|
||||
ShowMagic:
|
||||
a failed appraisal also prints "Spells: unknown."
|
||||
ordinary spell rows are:
|
||||
"Spell Descriptions:\n~ <name>: <DAT description>"
|
||||
enchantment rows are:
|
||||
"Enchantments:\n~ <name>: <DAT description>"
|
||||
|
||||
ShowLevelLimits:
|
||||
min == max: "Restricted to characters of Level %d."
|
||||
min + max: "Restricted to characters of Levels %d to %d."
|
||||
min only: "Restricted to characters of Level %d or greater."
|
||||
max only: "Restricted to characters of Level %d or below."
|
||||
|
||||
ShowItemLevel:
|
||||
require ItemBaseXp (int64 5), ItemMaxLevel (319), and ItemXpStyle (320)
|
||||
currentLevel = ItemTotalXPToLevel(ItemTotalXp, baseXp, maxLevel, style)
|
||||
displayedLevel = min(currentLevel + 1, maxLevel)
|
||||
nextThreshold = ItemLevelToTotalXP(min(currentLevel + 1, maxLevel), ...)
|
||||
"Item Level: <displayedLevel> / <maxLevel>"
|
||||
"Item XP: <grouped total> / <grouped nextThreshold>"
|
||||
|
||||
ItemLevelToTotalXP(level, base, max, style):
|
||||
clamp level to [0, max]
|
||||
style 1: base * level
|
||||
style 2: base * (2^level - 1)
|
||||
style 3: base * level * (level + 1) / 2
|
||||
|
||||
ShowActivationRequirements:
|
||||
successful appraisals only
|
||||
append Arcane Lore, Allegiance Rank, Heritage, skill, primary-attribute,
|
||||
and secondary-attribute requirements to one "Activation requires " row
|
||||
in that order
|
||||
|
||||
ShowBoostValue / ShowHealKitValues:
|
||||
ordinary consumables render Health/Mana "... when used."
|
||||
Stamina is "... when consumed."
|
||||
healer and hooked-healer profiles instead interpret BoostValue as
|
||||
"Bonus to Healing Skill" and float 100 as "Restoration Bonus"
|
||||
|
||||
ShowRareInfo:
|
||||
RareUsesTimer (bool 108):
|
||||
"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."
|
||||
RareId (17): "Rare #%d"
|
||||
|
||||
ShowDescription:
|
||||
if lifespan, creation timestamp, and remaining lifespan are present:
|
||||
remaining < 0: "This item is in the act of disintegrating."
|
||||
otherwise: "This item expires in <years/days/hours/minutes/seconds>"
|
||||
LongDesc replaces ShortDesc when present; both are never printed
|
||||
GearPlatingName (52) replaces LongDesc before decoration
|
||||
AppraisalItemSkill (172) decorates that description:
|
||||
bit 1 prepends workmanship
|
||||
MaterialType (131) prepends the DAT material display name and removes
|
||||
its first occurrence from the authored description
|
||||
bit 4 appends ", set with <count> <pluralized material>"
|
||||
PortalBitmask (111) appends, in bit order:
|
||||
Player Killers may not use this portal.
|
||||
Lite Player Killers may not use this portal.
|
||||
Non-Player Killers may not use this portal.
|
||||
This portal cannot be recalled nor linked to.
|
||||
This portal cannot be summoned.
|
||||
```
|
||||
|
||||
The material name is not a locally invented table.
|
||||
`MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500` performs
|
||||
`DBObj::GetByEnum(DBObjType=0x28, clientEnum=0x10000001, subEnum=1)`:
|
||||
the master map resolves the root `EnumIDMap`, sub-enum 1 resolves the material
|
||||
`DualEnumIDMap`, and its client name is read with underscores replaced by
|
||||
spaces. Creature/slayer
|
||||
names use `EnumMapper::GetString(0x10000005, value)`; heritage restrictions use
|
||||
`EnumMapper::GetString(0x10000002, value)` with retail's explicit
|
||||
Gharu'ndim/Umbraen/Olthoi spellings.
|
||||
|
||||
Additional named sources for these branches:
|
||||
|
||||
- `ItemExamineUI::Appraisal_ShowSet @ 0x004AE880`
|
||||
- `ItemExamineUI::Appraisal_ShowRatings @ 0x004AF080`
|
||||
- `ItemExamineUI::Appraisal_ShowTinkeringInfo @ 0x004B0E70`
|
||||
- `ItemExamineUI::Appraisal_ShowLevelLimitInfo @ 0x004B2920`
|
||||
- `ItemExamineUI::Appraisal_ShowItemLevelInfo @ 0x004B3C60`
|
||||
- `ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990`
|
||||
- `ExperienceSystem::ItemLevelToTotalXP @ 0x005C8100`
|
||||
- `ExperienceSystem::ItemTotalXPToLevel @ 0x005C81B0`
|
||||
- `MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500`
|
||||
|
||||
Sources:
|
||||
|
||||
- `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`
|
||||
|
|
@ -677,11 +810,13 @@ implements the same behavior.
|
|||
The request lifetime, exact packet flags, independent floaty window,
|
||||
inscription permission/edit/write transaction, subview choice, authored outer
|
||||
layout, ordered creature stats and ratings, animated creature preview,
|
||||
viewport/list/text compositing, selection-follow, scrollbar ownership, and
|
||||
combat refresh are in scope. Common item report rows now preserve retail's
|
||||
appraisal-only unknown values, hook/capacity/lock presence rules, exact
|
||||
paragraph boundaries, and authored normal/beneficial/detrimental font-color
|
||||
selection. Retail's item-object preview, exhaustive specialized
|
||||
`ItemExamineUI` detail branches, creature incomplete/high/low FontInfo
|
||||
selection, and PSR-only scribe-account/override behavior remain explicit
|
||||
AP-110 work.
|
||||
viewport/list/text compositing, selection-follow, scrollbar ownership, combat
|
||||
refresh, and the complete EoR item-report dispatch are in scope. Item rows now
|
||||
preserve retail's appraisal-only unknown values, hook/capacity/lock presence
|
||||
rules, exact paragraph boundaries, authored normal/beneficial/detrimental
|
||||
font-color selection, equipment-set/ratings/tinkering/weapon/armor/magic/XP/
|
||||
activation/healer/rare/description ordering, and installed-DAT material and
|
||||
creature names. Retail's item-object preview, live player-dependent effective
|
||||
shield/cooldown projections, localized augmentation-cost `StringInfo`,
|
||||
creature incomplete/high/low FontInfo selection, complete character detail,
|
||||
and PSR-only scribe-account/override behavior remain explicit AP-110 work.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private readonly CreatureAppraisalLayeredList? _creatureExtra;
|
||||
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
|
||||
private readonly CreatureDisplayNameResolver _creatureNames;
|
||||
private readonly RetailAppraisalNameResolver _itemNames;
|
||||
private AppraisalView _activeView;
|
||||
private uint _itemObjectId;
|
||||
private uint _creatureObjectId;
|
||||
|
|
@ -95,7 +96,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
UiText title,
|
||||
UiText itemText,
|
||||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates,
|
||||
CreatureDisplayNameResolver? creatureNames)
|
||||
CreatureDisplayNameResolver? creatureNames,
|
||||
RetailAppraisalNameResolver? itemNames)
|
||||
{
|
||||
_layout = layout;
|
||||
_objects = objects;
|
||||
|
|
@ -117,6 +119,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_creatureNames = creatureNames
|
||||
?? new CreatureDisplayNameResolver(
|
||||
new Dictionary<uint, string>());
|
||||
_itemNames = itemNames ?? RetailAppraisalNameResolver.Empty;
|
||||
_inscriptionText = layout.FindElement(InscriptionTextId) as UiText;
|
||||
_inscriptionField = layout.FindElement(InscriptionTextId) as UiField;
|
||||
_signature = layout.FindElement(SignatureTextId) as UiText;
|
||||
|
|
@ -200,7 +203,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
Action show,
|
||||
Action close,
|
||||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null,
|
||||
CreatureDisplayNameResolver? creatureNames = null)
|
||||
CreatureDisplayNameResolver? creatureNames = null,
|
||||
RetailAppraisalNameResolver? itemNames = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(objects);
|
||||
|
|
@ -237,7 +241,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
title,
|
||||
itemText,
|
||||
creatureRowTemplates,
|
||||
creatureNames);
|
||||
creatureNames,
|
||||
itemNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -343,7 +348,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_itemReport = ItemAppraisalTextFormatter.BuildReport(
|
||||
obj,
|
||||
appraisal,
|
||||
ResolveSpell);
|
||||
ResolveSpell,
|
||||
_itemNames);
|
||||
SetInscription(obj, appraisal);
|
||||
if (newlySelected)
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
91
src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs
Normal file
91
src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the authored enum display names consumed by retail's appraisal
|
||||
/// helpers. Material names follow
|
||||
/// <c>MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500</c>;
|
||||
/// creature and heritage names follow
|
||||
/// <c>AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0</c> and
|
||||
/// <c>InqHeritageGroupDisplayName @ 0x005B4710</c>.
|
||||
/// </summary>
|
||||
public sealed class RetailAppraisalNameResolver
|
||||
{
|
||||
private const uint MaterialClientEnum = 0x10000001u;
|
||||
private const uint MaterialSubEnum = 1u;
|
||||
|
||||
public static RetailAppraisalNameResolver Empty { get; } = new(
|
||||
new Dictionary<uint, string>(),
|
||||
new CreatureDisplayNameResolver(new Dictionary<uint, string>()));
|
||||
|
||||
private readonly IReadOnlyDictionary<uint, string> _materials;
|
||||
private readonly CreatureDisplayNameResolver _creatures;
|
||||
|
||||
public RetailAppraisalNameResolver(
|
||||
IReadOnlyDictionary<uint, string> materials,
|
||||
CreatureDisplayNameResolver creatures)
|
||||
{
|
||||
_materials = materials
|
||||
?? throw new ArgumentNullException(nameof(materials));
|
||||
_creatures = creatures
|
||||
?? throw new ArgumentNullException(nameof(creatures));
|
||||
}
|
||||
|
||||
public static RetailAppraisalNameResolver Load(
|
||||
IDatReaderWriter dats,
|
||||
CreatureDisplayNameResolver creatures)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(creatures);
|
||||
|
||||
var materials = new Dictionary<uint, string>();
|
||||
uint masterDid = (uint)dats.Portal.Db.Header.MasterMapId;
|
||||
EnumIDMap? master = masterDid == 0u
|
||||
? null
|
||||
: dats.Get<EnumIDMap>(masterDid);
|
||||
if (master is not null
|
||||
&& master.ClientEnumToID.TryGetValue(
|
||||
MaterialClientEnum,
|
||||
out uint materialRootMapDid)
|
||||
&& dats.Get<EnumIDMap>(materialRootMapDid) is { } materialRootMap
|
||||
&& materialRootMap.ClientEnumToID.TryGetValue(
|
||||
MaterialSubEnum,
|
||||
out uint materialMapDid)
|
||||
&& dats.Get<DualEnumIDMap>(materialMapDid) is { } materialMap)
|
||||
{
|
||||
foreach ((uint id, PStringBase<byte> text)
|
||||
in materialMap.ClientEnumToName)
|
||||
{
|
||||
materials.TryAdd(id, Normalize(text.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return new RetailAppraisalNameResolver(materials, creatures);
|
||||
}
|
||||
|
||||
public string ResolveCreature(int creatureType)
|
||||
=> _creatures.Resolve(creatureType);
|
||||
|
||||
public string ResolveHeritage(int heritageGroup)
|
||||
=> heritageGroup switch
|
||||
{
|
||||
2 => "Gharu'ndim",
|
||||
5 => "Umbraen",
|
||||
13 => "Olthoi",
|
||||
_ => CharacterIdentityText.HeritageGroupDisplayName(heritageGroup)
|
||||
?? string.Empty,
|
||||
};
|
||||
|
||||
public string ResolveMaterial(int materialType)
|
||||
=> materialType > 0
|
||||
&& _materials.TryGetValue((uint)materialType, out string? name)
|
||||
? name
|
||||
: string.Empty;
|
||||
|
||||
private static string Normalize(string value)
|
||||
=> value.Replace('_', ' ');
|
||||
}
|
||||
|
|
@ -966,6 +966,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ImportedLayout? layout;
|
||||
CreatureAppraisalRowTemplateFactory? creatureRows;
|
||||
CreatureDisplayNameResolver? creatureNames;
|
||||
RetailAppraisalNameResolver? itemNames;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
layout = LayoutImporter.Import(
|
||||
|
|
@ -982,6 +983,9 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.ResolveFont);
|
||||
creatureNames = CreatureDisplayNameResolver.Load(
|
||||
_bindings.Assets.Dats);
|
||||
itemNames = RetailAppraisalNameResolver.Load(
|
||||
_bindings.Assets.Dats,
|
||||
creatureNames);
|
||||
}
|
||||
if (layout is null)
|
||||
{
|
||||
|
|
@ -1003,7 +1007,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
show: () => Host.ShowWindow(WindowNames.Examination),
|
||||
close: () => CloseWindow(WindowNames.Examination),
|
||||
creatureRowTemplates: creatureRows,
|
||||
creatureNames: creatureNames);
|
||||
creatureNames: creatureNames,
|
||||
itemNames: itemNames);
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public enum PublicWeenieFlags : uint
|
|||
NonPlayerKillerSwitch = 0x00000800,
|
||||
Door = 0x00001000,
|
||||
Corpse = 0x00002000,
|
||||
Healer = 0x00010000,
|
||||
Lockpick = 0x00020000,
|
||||
RequiresPackSlot = 0x00800000,
|
||||
VolatileRare = 0x10000000,
|
||||
WieldOnUse = 0x20000000,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
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;
|
||||
|
|
@ -63,21 +64,24 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
});
|
||||
|
||||
Assert.Contains("Value: 12,500", report);
|
||||
Assert.Contains("Workmanship: Utterly flawless (8.4)", 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, Impenetrability VI", 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\nHurls a bolt of flame.", report);
|
||||
Assert.Contains("~ Flame Bolt VI: Hurls a bolt of flame.", report);
|
||||
Assert.Contains("Enchantments:", report);
|
||||
Assert.Contains("Impenetrability VI\nIncreases the target's armor.", report);
|
||||
Assert.Contains(
|
||||
"~ Impenetrability VI: Increases the target's armor.",
|
||||
report);
|
||||
Assert.True(
|
||||
report.IndexOf("Skill:", StringComparison.Ordinal)
|
||||
< report.IndexOf("Spells:", StringComparison.Ordinal));
|
||||
|
|
@ -353,6 +357,7 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
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(
|
||||
|
|
@ -372,7 +377,7 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
appraisal,
|
||||
_ => null);
|
||||
|
||||
Assert.Contains("Ratings: Dam +3", report);
|
||||
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);
|
||||
|
|
@ -461,17 +466,260 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
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)
|
||||
(ushort Highlight, ushort Color)? weaponEnchantments = null,
|
||||
bool success = true)
|
||||
=> new(
|
||||
Guid: 0x50000001u,
|
||||
Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable,
|
||||
Success: true,
|
||||
Success: success,
|
||||
Properties: properties,
|
||||
SpellBook: spells ?? [],
|
||||
ArmorProfile: armor,
|
||||
|
|
@ -511,4 +759,44 @@ public sealed class ItemAppraisalTextFormatterTests
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue