fix(ui): match retail item appraisal semantics
Preserve PublicWeenieDesc hook identity from CreateObject through the item model so hook appraisals suppress sentinel capacities exactly. Use appraisal-only Value and Burden presence, retain AddItemInfo paragraph and authored font-color selection, and port retail lock, page, enchantment, and spell-block formatting. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
bc47bc4917
commit
d3c5e06fdd
21 changed files with 982 additions and 145 deletions
|
|
@ -107,7 +107,21 @@ text out from the top, and replaces the sparse property dump with retail's
|
||||||
ordered item-report branches for common weapons, armor, magic and DAT spell
|
ordered item-report branches for common weapons, armor, magic and DAT spell
|
||||||
descriptions, requirements, capacities, cooldown/special properties, uses,
|
descriptions, requirements, capacities, cooldown/special properties, uses,
|
||||||
crafting, ratings, rare state, and prose. AP-110 records the remaining
|
crafting, ratings, rare state, and prose. AP-110 records the remaining
|
||||||
specialized/player-dependent/DAT-name/font/object-preview branches explicitly.
|
specialized/player-dependent/DAT-name/creature-font/object-preview branches
|
||||||
|
explicitly.
|
||||||
|
|
||||||
|
The item-format conformance correction then fixed the live Black Phyntos Hive
|
||||||
|
counterexample at its source. CreateObject had walked past
|
||||||
|
`PublicWeenieDesc.HookItemTypes` and `HookType` without retaining them, so the
|
||||||
|
item formatter could not execute retail `ACCWeenieObject::IsHook`. It also
|
||||||
|
fell back from a deliberately incomplete appraisal to public Value/Burden and
|
||||||
|
printed the hook's 255/255 sentinel capacities as ordinary storage. Hook
|
||||||
|
identity now survives the wire/session/object-table path; Value/Burden use
|
||||||
|
only the appraisal profile and display `???`/`Unknown` when absent;
|
||||||
|
`HasHookedItem` suppresses the hook capacity; lock wording and page-count
|
||||||
|
property order match retail; and each report fragment preserves
|
||||||
|
`AddItemInfo`'s line/paragraph flag plus LayoutDesc `0x1B`'s white/green/red
|
||||||
|
font-color indices.
|
||||||
|
|
||||||
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
||||||
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
||||||
|
|
@ -129,6 +143,9 @@ animated target remains above row chrome but below text; changing selection
|
||||||
updates the open window without another magnifier click. Item text begins at
|
updates the open window without another magnifier click. Item text begins at
|
||||||
the top and assessed melee/missile/armor/magic items show their retail-ordered
|
the top and assessed melee/missile/armor/magic items show their retail-ordered
|
||||||
stats and full DAT spell descriptions.
|
stats and full DAT spell descriptions.
|
||||||
|
The Black Phyntos Hive specifically reads `Value: ???`, `Burden: Unknown`,
|
||||||
|
then its description after one retail paragraph break, without the bogus
|
||||||
|
255-item/255-container line.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-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-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-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. 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 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 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::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 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-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-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-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` |
|
| 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` |
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,11 @@ damage/critical/resistance rating list, places authored row chrome behind the
|
||||||
animated preview and text in front, adds the balanced row inset, and follows
|
animated preview and text in front, adds the balanced row inset, and follows
|
||||||
the current selection automatically while the examination window is visible.
|
the current selection automatically while the examination window is visible.
|
||||||
Item-object preview, specialized item/character detail regions, and exact
|
Item-object preview, specialized item/character detail regions, and exact
|
||||||
appraisal font-state selection remain the narrowed AP-110 residual. Research:
|
creature appraisal font-state selection remain the narrowed AP-110 residual.
|
||||||
|
The item report now retains `PublicWeenieDesc` hook identity, applies
|
||||||
|
appraisal-only Value/Burden unknowns, suppresses mounted-hook sentinel
|
||||||
|
capacities, preserves retail line/paragraph boundaries, and selects the
|
||||||
|
authored white/green/red item font-color entries. Research:
|
||||||
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
|
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
|
||||||
The focused parser/router/request/controller/fixture tests pass, as do the
|
The focused parser/router/request/controller/fixture tests pass, as do the
|
||||||
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
|
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
|
||||||
|
|
@ -172,14 +176,26 @@ ports common weapon damage ranges/speed/range/ammunition, armor protection
|
||||||
bands, defense/caster modifiers, workmanship, ratings, wield/use/activation
|
bands, defense/caster modifiers, workmanship, ratings, wield/use/activation
|
||||||
requirements, item XP/capacity/lock/mana/uses/creator/rare data, cooldown and
|
requirements, item XP/capacity/lock/mana/uses/creator/rare data, cooldown and
|
||||||
imbued special properties, and both short spell lists and full DAT spell
|
imbued special properties, and both short spell lists and full DAT spell
|
||||||
descriptions. AP-110 now names only the remaining specialized,
|
descriptions. The conformance follow-up replaces public-value fallbacks with
|
||||||
player-dependent, DAT-display-name, font-state, and object-preview gaps.
|
retail's appraisal presence semantics, restores hook/capacity/lock behavior,
|
||||||
|
and carries each `AddItemInfo` fragment's paragraph and font-color index into
|
||||||
|
the retained text shaper. AP-110 now names only the remaining specialized,
|
||||||
|
player-dependent, DAT-display-name, creature-font-state, and object-preview gaps.
|
||||||
Focused conformance fixtures cover the geometry layering plus melee, launcher,
|
Focused conformance fixtures cover the geometry layering plus melee, launcher,
|
||||||
armor, spell, and special-property reports. The Release solution build passes
|
armor, spell, and special-property reports. The Release solution build passes
|
||||||
with the 17 warnings already tracked by #228, 3,514 App tests / 3 skips, and
|
with the 17 warnings already tracked by #228, 3,514 App tests / 3 skips, and
|
||||||
7,931 complete-solution tests / 5 skips. The connected visual result remains
|
7,931 complete-solution tests / 5 skips. The connected visual result remains
|
||||||
the closeout gate.
|
the closeout gate.
|
||||||
|
|
||||||
|
The item-format conformance correction adds literal hook-tail cursor fixtures,
|
||||||
|
wire/session/object-table propagation tests, the exact Black Phyntos Hive
|
||||||
|
report, capacity/page/lock presence cases, structured paragraph checks, and
|
||||||
|
LayoutDesc color-palette/enchantment-style coverage. The isolated Release
|
||||||
|
solution build passes with the same 17 tracked warnings; 3,522 App tests / 3
|
||||||
|
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.
|
||||||
|
|
||||||
## Slice 1 — spell-bar overflow arrows
|
## Slice 1 — spell-bar overflow arrows
|
||||||
|
|
||||||
### Retail oracle
|
### Retail oracle
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,16 @@ retained examination window.
|
||||||
- `Attribute2ndInfoRegion::Update @ 0x004F1E80`
|
- `Attribute2ndInfoRegion::Update @ 0x004F1E80`
|
||||||
- `AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0`
|
- `AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0`
|
||||||
- `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`
|
- `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`
|
||||||
|
- `ItemExamineUI::AddItemInfo @ 0x004AC050/0x004ADCA0`
|
||||||
|
- `ItemExamineUI::Appraisal_ShowValueInfo @ 0x004ADEA0`
|
||||||
|
- `ItemExamineUI::Appraisal_ShowBurdenInfo @ 0x004AE180`
|
||||||
|
- `ItemExamineUI::Appraisal_ShowCapacity @ 0x004B2680`
|
||||||
|
- `ItemExamineUI::Appraisal_ShowLockAppraiseInfo @ 0x004B2790`
|
||||||
|
- `AppraisalProfile::HasHookedItem @ 0x00525C70`
|
||||||
|
- `AppraisalProfile::InqIntEnchantmentMod @ 0x005B2C10`
|
||||||
|
- `AppraisalProfile::InqFloatEnchantmentMod @ 0x005B2C70`
|
||||||
|
- `AppraisalSystem::LockpickSuccessPercentToString @ 0x005B4600`
|
||||||
|
- `ACCWeenieObject::IsHook @ 0x0058C660`
|
||||||
- `ItemExamineUI::HandleInscriptionGainingFocus @ 0x004AC0E0`
|
- `ItemExamineUI::HandleInscriptionGainingFocus @ 0x004AC0E0`
|
||||||
- `ItemExamineUI::HandleInscriptionMousePresses @ 0x004AC230`
|
- `ItemExamineUI::HandleInscriptionMousePresses @ 0x004AC230`
|
||||||
- `ItemExamineUI::SetInscriptionEditableState @ 0x004AC720`
|
- `ItemExamineUI::SetInscriptionEditableState @ 0x004AC720`
|
||||||
|
|
@ -333,6 +343,57 @@ The individual branches are not a generic property dump. They project the
|
||||||
wire profile into retail prose:
|
wire profile into retail prose:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
AddItemInfo(text, fontColorIndex, sameParagraph):
|
||||||
|
if the report already contains text:
|
||||||
|
append sameParagraph ? "\n" : "\n\n"
|
||||||
|
append text with font-list index 0 and the requested color-list index
|
||||||
|
|
||||||
|
ShowValueAndBurden(profile):
|
||||||
|
if profile contains int 19:
|
||||||
|
append "Value: <grouped value>"
|
||||||
|
else:
|
||||||
|
append "Value: ???"
|
||||||
|
|
||||||
|
if profile contains int 5:
|
||||||
|
append "Burden: <grouped burden>"
|
||||||
|
else:
|
||||||
|
append "Burden: Unknown"
|
||||||
|
|
||||||
|
IsHook(object):
|
||||||
|
return PublicWeenieDesc.HookType != 0
|
||||||
|
and PublicWeenieDesc.HookItemTypes != TYPE_UNDEF
|
||||||
|
|
||||||
|
ShowCapacity(object, profile):
|
||||||
|
if object is not a hook or profile does not contain a HookProfile:
|
||||||
|
append authored item/container capacity as a new paragraph
|
||||||
|
if profile contains PageCount (175) and PageUsed (174):
|
||||||
|
append "<used> of <count> pages full." as a new paragraph
|
||||||
|
|
||||||
|
# A hook with a HookProfile describes its mounted item. Its sentinel
|
||||||
|
# 255/255 PublicWeenieDesc capacities are not player-facing capacity.
|
||||||
|
|
||||||
|
ShowLock(object, profile):
|
||||||
|
if object is a hook:
|
||||||
|
return
|
||||||
|
if Locked bool is absent:
|
||||||
|
append nonzero LockpickResistance as
|
||||||
|
"Bonus to Lockpick Skill: <signed value>"
|
||||||
|
return
|
||||||
|
append "Unlocked" and return when Locked is false
|
||||||
|
append "Locked"
|
||||||
|
if LockpickResistance is absent:
|
||||||
|
append "You can't tell how hard the lock is to pick."
|
||||||
|
else if LockpickSuccessPercent is present:
|
||||||
|
map it through LockpickSuccessPercentToString
|
||||||
|
append "The lock looks <grade> to pick (Resistance <value>)."
|
||||||
|
|
||||||
|
FontColor(profileBitfield, lowBit):
|
||||||
|
if lowBit is absent:
|
||||||
|
return normal color index 0
|
||||||
|
if the corresponding bit 16 positions higher is present:
|
||||||
|
return beneficial color index 1
|
||||||
|
return detrimental color index 2
|
||||||
|
|
||||||
ShowWeaponAndArmor(validLocations, profile):
|
ShowWeaponAndArmor(validLocations, profile):
|
||||||
ammoType = live PublicWeenieDesc ammo type
|
ammoType = live PublicWeenieDesc ammo type
|
||||||
if object is a hook:
|
if object is a hook:
|
||||||
|
|
@ -470,6 +531,10 @@ The main text and inscription areas use their authored scrollbars. A newly
|
||||||
selected object resets those scroll positions to the top; a refresh of the
|
selected object resets those scroll positions to the top; a refresh of the
|
||||||
same object preserves the user's position.
|
same object preserves the user's position.
|
||||||
|
|
||||||
|
LayoutDesc `0x2100006B` element `0x1000013C` authors the item report's
|
||||||
|
font-color list as white, green, and red. Those are the concrete colors selected
|
||||||
|
by the `AddItemInfo` indices above; they are not hard-coded by appraisal logic.
|
||||||
|
|
||||||
## Window behavior
|
## Window behavior
|
||||||
|
|
||||||
The LayoutDesc root already contains retail's complete floaty window chrome,
|
The LayoutDesc root already contains retail's complete floaty window chrome,
|
||||||
|
|
@ -603,7 +668,10 @@ The request lifetime, exact packet flags, independent floaty window,
|
||||||
inscription permission/edit/write transaction, subview choice, authored outer
|
inscription permission/edit/write transaction, subview choice, authored outer
|
||||||
layout, ordered creature stats and ratings, animated creature preview,
|
layout, ordered creature stats and ratings, animated creature preview,
|
||||||
viewport/list/text compositing, selection-follow, scrollbar ownership, and
|
viewport/list/text compositing, selection-follow, scrollbar ownership, and
|
||||||
combat refresh are in scope. Retail's item-object preview, exhaustive
|
combat refresh are in scope. Common item report rows now preserve retail's
|
||||||
specialized `ItemExamineUI` detail branches, exact appraisal FontInfo-list
|
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
|
selection, and PSR-only scribe-account/override behavior remain explicit
|
||||||
AP-110 work.
|
AP-110 work.
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ Per-flag (in exact order):
|
||||||
0x00400000 SpellId : uint16
|
0x00400000 SpellId : uint16
|
||||||
0x02000000 OwnerId : uint32
|
0x02000000 OwnerId : uint32
|
||||||
0x04000000 Restrictions : RestrictionDB (house perms)
|
0x04000000 Restrictions : RestrictionDB (house perms)
|
||||||
0x20000000 HookItemTypes : uint16 (HookType)
|
0x20000000 HookItemTypes : uint32 (ITEM_TYPE mask)
|
||||||
0x00000040 MonarchId : uint32
|
0x00000040 MonarchId : uint32
|
||||||
0x10000000 HookType : uint16
|
0x10000000 HookType : uint16
|
||||||
0x40000000 IconOverlay : packedDWORD (minus 0x06000000)
|
0x40000000 IconOverlay : packedDWORD (minus 0x06000000)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
private uint _creatureObjectId;
|
private uint _creatureObjectId;
|
||||||
private uint _characterObjectId;
|
private uint _characterObjectId;
|
||||||
private string _titleValue = string.Empty;
|
private string _titleValue = string.Empty;
|
||||||
private string _itemReport = string.Empty;
|
private ItemAppraisalReport _itemReport = ItemAppraisalReport.Empty;
|
||||||
private string _inscriptionValue = string.Empty;
|
private string _inscriptionValue = string.Empty;
|
||||||
private string _signatureValue = string.Empty;
|
private string _signatureValue = string.Empty;
|
||||||
private string _scribeName = string.Empty;
|
private string _scribeName = string.Empty;
|
||||||
|
|
@ -131,7 +131,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
// LayoutDesc 0x2100006B carries Bottom here, which is appropriate for
|
// LayoutDesc 0x2100006B carries Bottom here, which is appropriate for
|
||||||
// static text but not for the generated retail item list.
|
// static text but not for the generated retail item list.
|
||||||
_itemText.VerticalJustify = VJustify.Top;
|
_itemText.VerticalJustify = VJustify.Top;
|
||||||
ConfigureScrollableText(_itemText, ItemScrollbarId, () => _itemReport);
|
ConfigureScrollableItemText(_itemText, ItemScrollbarId);
|
||||||
if (_inscriptionText is not null)
|
if (_inscriptionText is not null)
|
||||||
ConfigureScrollableText(
|
ConfigureScrollableText(
|
||||||
_inscriptionText,
|
_inscriptionText,
|
||||||
|
|
@ -311,7 +311,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
public void ResetSession()
|
public void ResetSession()
|
||||||
{
|
{
|
||||||
_titleValue = string.Empty;
|
_titleValue = string.Empty;
|
||||||
_itemReport = string.Empty;
|
_itemReport = ItemAppraisalReport.Empty;
|
||||||
_inscriptionValue = string.Empty;
|
_inscriptionValue = string.Empty;
|
||||||
_inscriptionField?.SetText(string.Empty);
|
_inscriptionField?.SetText(string.Empty);
|
||||||
if (_inscriptionField is not null)
|
if (_inscriptionField is not null)
|
||||||
|
|
@ -340,7 +340,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
AppraiseInfoParser.Parsed appraisal,
|
AppraiseInfoParser.Parsed appraisal,
|
||||||
bool newlySelected)
|
bool newlySelected)
|
||||||
{
|
{
|
||||||
_itemReport = ItemAppraisalTextFormatter.Build(
|
_itemReport = ItemAppraisalTextFormatter.BuildReport(
|
||||||
obj,
|
obj,
|
||||||
appraisal,
|
appraisal,
|
||||||
ResolveSpell);
|
ResolveSpell);
|
||||||
|
|
@ -370,8 +370,9 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
(PublicWeenieFlags)(obj.PublicWeenieBitfield ?? 0u);
|
(PublicWeenieFlags)(obj.PublicWeenieBitfield ?? 0u);
|
||||||
bool publicInscribable =
|
bool publicInscribable =
|
||||||
(publicFlags & PublicWeenieFlags.Inscribable) != 0;
|
(publicFlags & PublicWeenieFlags.Inscribable) != 0;
|
||||||
_presentationInscribable = appraisal.HookProfile is { } hook
|
_presentationInscribable = obj.IsHook
|
||||||
? (hook.Flags & 0x1u) != 0
|
? appraisal.HookProfile is { } hook
|
||||||
|
&& (hook.Flags & 0x1u) != 0
|
||||||
: publicInscribable;
|
: publicInscribable;
|
||||||
|
|
||||||
if (!_presentationInscribable)
|
if (!_presentationInscribable)
|
||||||
|
|
@ -559,6 +560,19 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
scrollbar.Model = text.Scroll;
|
scrollbar.Model = text.Scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ConfigureScrollableItemText(
|
||||||
|
UiText text,
|
||||||
|
uint scrollbarId)
|
||||||
|
{
|
||||||
|
text.PreserveEndOnLayout = false;
|
||||||
|
text.WheelScrollEnabled = true;
|
||||||
|
text.ClickThrough = false;
|
||||||
|
text.LinesProvider = () =>
|
||||||
|
ItemAppraisalTextLayout.Shape(text, _itemReport);
|
||||||
|
if (_layout.FindElement(scrollbarId) is UiScrollbar scrollbar)
|
||||||
|
scrollbar.Model = text.Scroll;
|
||||||
|
}
|
||||||
|
|
||||||
private void SetText(uint elementId, string value, bool scrollable = false)
|
private void SetText(uint elementId, string value, bool scrollable = false)
|
||||||
{
|
{
|
||||||
if (_layout.FindElement(elementId) is not UiText text)
|
if (_layout.FindElement(elementId) is not UiText text)
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,9 @@ public static class DatWidgetFactory
|
||||||
// it is the shared global fallback. Controllers that call FindElement and explicitly
|
// it is the shared global fallback. Controllers that call FindElement and explicitly
|
||||||
// set DatFont afterward STILL override this (backward-compat guarantee).
|
// set DatFont afterward STILL override this (backward-compat guarantee).
|
||||||
DatFont = elementFont,
|
DatFont = elementFont,
|
||||||
|
FontColorPalette = ElementReader.ReadEffectiveColorPalette(
|
||||||
|
info,
|
||||||
|
0x1Bu),
|
||||||
};
|
};
|
||||||
t.ConfigureDatState(info);
|
t.ConfigureDatState(info);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -385,4 +385,38 @@ public static class ElementReader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves an authored color or color-array property after the retail
|
||||||
|
/// DirectState/default-state inheritance rules have been applied.
|
||||||
|
/// </summary>
|
||||||
|
internal static Vector4[] ReadEffectiveColorPalette(
|
||||||
|
ElementInfo info,
|
||||||
|
uint propertyId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(info);
|
||||||
|
if (!info.TryGetEffectiveProperty(propertyId, out UiPropertyValue value))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
IEnumerable<UiPropertyValue> entries = value.Kind switch
|
||||||
|
{
|
||||||
|
UiPropertyKind.Color => [value],
|
||||||
|
UiPropertyKind.Array => value.ArrayValue,
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.Where(entry => entry.Kind == UiPropertyKind.Color)
|
||||||
|
.Select(entry =>
|
||||||
|
{
|
||||||
|
UiColorValue color = entry.ColorValue;
|
||||||
|
float alpha = color.Alpha == 0 ? 1f : color.Alpha / 255f;
|
||||||
|
return new Vector4(
|
||||||
|
color.Red / 255f,
|
||||||
|
color.Green / 255f,
|
||||||
|
color.Blue / 255f,
|
||||||
|
alpha);
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
src/AcDream.App/UI/Layout/ItemAppraisalReport.cs
Normal file
177
src/AcDream.App/UI/Layout/ItemAppraisalReport.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Font-color indices selected by retail <c>ItemExamineUI</c>. The concrete
|
||||||
|
/// colors come from the item report's LayoutDesc property <c>0x1B</c>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ItemAppraisalFontStyle
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
Beneficial = 1,
|
||||||
|
Detrimental = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Separator inserted before an appraisal fragment. This is the retained
|
||||||
|
/// equivalent of <c>ItemExamineUI::AddItemInfo</c>'s final argument.
|
||||||
|
/// </summary>
|
||||||
|
public enum ItemAppraisalSeparator
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Line,
|
||||||
|
Paragraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One retail appraisal append operation: prose, separator, and authored
|
||||||
|
/// font-color index. Keeping these separate until shaping preserves style
|
||||||
|
/// when a long fragment wraps.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ItemAppraisalFragment(
|
||||||
|
string Text,
|
||||||
|
ItemAppraisalSeparator Separator,
|
||||||
|
ItemAppraisalFontStyle Style);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable item appraisal report in retail append order.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ItemAppraisalReport
|
||||||
|
{
|
||||||
|
public static ItemAppraisalReport Empty { get; } = new([]);
|
||||||
|
|
||||||
|
public ItemAppraisalReport(IReadOnlyList<ItemAppraisalFragment> fragments)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(fragments);
|
||||||
|
Fragments = fragments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ItemAppraisalFragment> Fragments { get; }
|
||||||
|
public bool IsEmpty => Fragments.Count == 0;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var text = new StringBuilder();
|
||||||
|
foreach (ItemAppraisalFragment fragment in Fragments)
|
||||||
|
{
|
||||||
|
if (text.Length != 0)
|
||||||
|
{
|
||||||
|
text.Append(fragment.Separator == ItemAppraisalSeparator.Paragraph
|
||||||
|
? "\n\n"
|
||||||
|
: "\n");
|
||||||
|
}
|
||||||
|
text.Append(fragment.Text);
|
||||||
|
}
|
||||||
|
return text.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>ItemExamineUI::AddItemInfo @ 0x004AC050/0x004ADCA0</c>.
|
||||||
|
/// Retail appends one newline when <paramref name="sameParagraph"/> is true,
|
||||||
|
/// two otherwise, then applies font DID index zero and the supplied color index.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ItemAppraisalReportBuilder
|
||||||
|
{
|
||||||
|
private readonly List<ItemAppraisalFragment> _fragments = [];
|
||||||
|
|
||||||
|
public void Line(
|
||||||
|
string value,
|
||||||
|
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
|
||||||
|
=> Add(value, sameParagraph: true, style);
|
||||||
|
|
||||||
|
public void Paragraph(
|
||||||
|
string value,
|
||||||
|
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
|
||||||
|
=> Add(value, sameParagraph: false, style);
|
||||||
|
|
||||||
|
public ItemAppraisalReport Build()
|
||||||
|
=> _fragments.Count == 0
|
||||||
|
? ItemAppraisalReport.Empty
|
||||||
|
: new ItemAppraisalReport(_fragments.ToArray());
|
||||||
|
|
||||||
|
private void Add(
|
||||||
|
string value,
|
||||||
|
bool sameParagraph,
|
||||||
|
ItemAppraisalFontStyle style)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_fragments.Add(new ItemAppraisalFragment(
|
||||||
|
value,
|
||||||
|
_fragments.Count == 0
|
||||||
|
? ItemAppraisalSeparator.None
|
||||||
|
: sameParagraph
|
||||||
|
? ItemAppraisalSeparator.Line
|
||||||
|
: ItemAppraisalSeparator.Paragraph,
|
||||||
|
style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Width-aware projection of retail appraisal fragments into the retained
|
||||||
|
/// <see cref="UiText"/> line model.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ItemAppraisalTextLayout
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<UiText.Line> Shape(
|
||||||
|
UiText target,
|
||||||
|
ItemAppraisalReport report)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
ArgumentNullException.ThrowIfNull(report);
|
||||||
|
|
||||||
|
float maxWidth = Math.Max(1f, target.Width - (2f * target.Padding));
|
||||||
|
float Measure(string value)
|
||||||
|
=> target.DatFont?.MeasureWidth(value)
|
||||||
|
?? target.Font?.MeasureWidth(value)
|
||||||
|
?? value.Length * 8f;
|
||||||
|
|
||||||
|
var lines = new List<UiText.Line>();
|
||||||
|
foreach (ItemAppraisalFragment fragment in report.Fragments)
|
||||||
|
{
|
||||||
|
if (lines.Count != 0
|
||||||
|
&& fragment.Separator == ItemAppraisalSeparator.Paragraph)
|
||||||
|
{
|
||||||
|
lines.Add(new UiText.Line(string.Empty, ResolveColor(target, fragment.Style)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector4 color = ResolveColor(target, fragment.Style);
|
||||||
|
string normalized = fragment.Text.Replace(
|
||||||
|
"\\n",
|
||||||
|
"\n",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
foreach (string logicalLine in normalized.Split('\n'))
|
||||||
|
{
|
||||||
|
if (logicalLine.Length == 0)
|
||||||
|
{
|
||||||
|
lines.Add(new UiText.Line(string.Empty, color));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string wrapped in ChatWindowController.WrapText(
|
||||||
|
logicalLine,
|
||||||
|
maxWidth,
|
||||||
|
Measure))
|
||||||
|
{
|
||||||
|
lines.Add(new UiText.Line(wrapped, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 ResolveColor(
|
||||||
|
UiText target,
|
||||||
|
ItemAppraisalFontStyle style)
|
||||||
|
{
|
||||||
|
int index = (int)style;
|
||||||
|
return index >= 0 && index < target.FontColorPalette.Count
|
||||||
|
? target.FontColorPalette[index]
|
||||||
|
: target.DefaultColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,12 @@ public static class ItemAppraisalTextFormatter
|
||||||
ClientObject obj,
|
ClientObject obj,
|
||||||
AppraiseInfoParser.Parsed appraisal,
|
AppraiseInfoParser.Parsed appraisal,
|
||||||
Func<uint, SpellMetadata?> resolveSpell)
|
Func<uint, SpellMetadata?> resolveSpell)
|
||||||
|
=> BuildReport(obj, appraisal, resolveSpell).ToString();
|
||||||
|
|
||||||
|
public static ItemAppraisalReport BuildReport(
|
||||||
|
ClientObject obj,
|
||||||
|
AppraiseInfoParser.Parsed appraisal,
|
||||||
|
Func<uint, SpellMetadata?> resolveSpell)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(obj);
|
ArgumentNullException.ThrowIfNull(obj);
|
||||||
ArgumentNullException.ThrowIfNull(resolveSpell);
|
ArgumentNullException.ThrowIfNull(resolveSpell);
|
||||||
|
|
@ -35,11 +41,11 @@ public static class ItemAppraisalTextFormatter
|
||||||
var report = new RetailReportBuilder();
|
var report = new RetailReportBuilder();
|
||||||
|
|
||||||
// ItemExamineUI::SetAppraiseInfo @ 0x004B72B0.
|
// ItemExamineUI::SetAppraiseInfo @ 0x004B72B0.
|
||||||
ShowValueAndBurden(report, obj, properties);
|
ShowValueAndBurden(report, properties);
|
||||||
ShowTinkering(report, obj, properties);
|
ShowTinkering(report, obj, properties);
|
||||||
ShowSetAndRatings(report, properties);
|
ShowSetAndRatings(report, properties);
|
||||||
ShowWeaponAndArmor(report, obj, appraisal);
|
ShowWeaponAndArmor(report, obj, appraisal);
|
||||||
ShowDefenseModifiers(report, properties);
|
ShowDefenseModifiers(report, appraisal);
|
||||||
ShowShortMagicInfo(report, appraisal, resolveSpell);
|
ShowShortMagicInfo(report, appraisal, resolveSpell);
|
||||||
ShowSpecialProperties(report, obj, properties);
|
ShowSpecialProperties(report, obj, properties);
|
||||||
ShowUsage(report, properties);
|
ShowUsage(report, properties);
|
||||||
|
|
@ -48,9 +54,9 @@ public static class ItemAppraisalTextFormatter
|
||||||
ShowUsageLimits(report, properties);
|
ShowUsageLimits(report, properties);
|
||||||
ShowItemLevel(report, properties);
|
ShowItemLevel(report, properties);
|
||||||
ShowActivationRequirements(report, properties);
|
ShowActivationRequirements(report, properties);
|
||||||
ShowCasterData(report, properties);
|
ShowCasterData(report, appraisal);
|
||||||
ShowBoostAndHealing(report, properties);
|
ShowBoostAndHealing(report, properties);
|
||||||
ShowCapacityAndLock(report, obj, properties);
|
ShowCapacityAndLock(report, obj, appraisal);
|
||||||
ShowManaStone(report, appraisal);
|
ShowManaStone(report, appraisal);
|
||||||
ShowRemainingUses(report, obj, appraisal);
|
ShowRemainingUses(report, obj, appraisal);
|
||||||
ShowCraftsman(report, properties);
|
ShowCraftsman(report, properties);
|
||||||
|
|
@ -58,22 +64,23 @@ public static class ItemAppraisalTextFormatter
|
||||||
ShowMagicInfo(report, appraisal, resolveSpell);
|
ShowMagicInfo(report, appraisal, resolveSpell);
|
||||||
ShowDescription(report, properties);
|
ShowDescription(report, properties);
|
||||||
|
|
||||||
if (!appraisal.Success && report.IsEmpty)
|
return report.Build();
|
||||||
report.Paragraph("Assessment incomplete");
|
|
||||||
return report.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowValueAndBurden(
|
private static void ShowValueAndBurden(
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
ClientObject obj,
|
|
||||||
PropertyBundle properties)
|
PropertyBundle properties)
|
||||||
{
|
{
|
||||||
int value = obj.Value != 0 ? obj.Value : properties.GetInt(19u);
|
// Appraisal_ShowValueInfo @ 0x004ADEA0 and
|
||||||
int burden = obj.Burden != 0 ? obj.Burden : properties.GetInt(5u);
|
// Appraisal_ShowBurdenInfo @ 0x004AE180 query only the appraisal
|
||||||
if (value != 0)
|
// profile. PublicWeenieDesc values must not leak through a failed or
|
||||||
report.Line($"Value: {value.ToString("N0", CultureInfo.CurrentCulture)}");
|
// deliberately incomplete assessment.
|
||||||
if (burden != 0)
|
report.Line(properties.Ints.TryGetValue(19u, out int value)
|
||||||
report.Line($"Burden: {burden.ToString("N0", CultureInfo.CurrentCulture)}");
|
? $"Value: {value.ToString("N0", CultureInfo.InvariantCulture)}"
|
||||||
|
: "Value: ???");
|
||||||
|
report.Line(properties.Ints.TryGetValue(5u, out int burden)
|
||||||
|
? $"Burden: {burden.ToString("N0", CultureInfo.InvariantCulture)}"
|
||||||
|
: "Burden: Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -161,15 +168,18 @@ public static class ItemAppraisalTextFormatter
|
||||||
{
|
{
|
||||||
PropertyBundle properties = appraisal.Properties;
|
PropertyBundle properties = appraisal.Properties;
|
||||||
uint validLocations = (uint)obj.ValidLocations;
|
uint validLocations = (uint)obj.ValidLocations;
|
||||||
uint ammoType = appraisal.HookProfile?.AmmoType
|
uint ammoType = obj.IsHook && appraisal.HookProfile is { } hook
|
||||||
?? obj.AmmoType
|
? hook.AmmoType
|
||||||
?? 0u;
|
: obj.AmmoType ?? 0u;
|
||||||
|
|
||||||
if ((validLocations & (uint)EquipMask.Shield) != 0)
|
if ((validLocations & (uint)EquipMask.Shield) != 0)
|
||||||
{
|
{
|
||||||
if (properties.Ints.TryGetValue(28u, out int shieldLevel))
|
if (properties.Ints.TryGetValue(28u, out int shieldLevel))
|
||||||
report.Line(
|
report.Line(
|
||||||
$"Base Shield Level: {shieldLevel.ToString(CultureInfo.CurrentCulture)}");
|
$"Base Shield Level: {shieldLevel.ToString(CultureInfo.CurrentCulture)}",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.ArmorEnchantments,
|
||||||
|
0x0001u));
|
||||||
else if (!appraisal.Success)
|
else if (!appraisal.Success)
|
||||||
report.Line("Shield Level: Unknown");
|
report.Line("Shield Level: Unknown");
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +201,16 @@ public static class ItemAppraisalTextFormatter
|
||||||
: weapon.Damage.ToString(CultureInfo.CurrentCulture);
|
: weapon.Damage.ToString(CultureInfo.CurrentCulture);
|
||||||
if (!launcher)
|
if (!launcher)
|
||||||
damage += $", {DamageTypeName(weapon.DamageType)}";
|
damage += $", {DamageTypeName(weapon.DamageType)}";
|
||||||
report.Line($"{damageLabel}: {damage}");
|
ItemAppraisalFontStyle damageStyle = EnchantmentStyle(
|
||||||
|
appraisal.WeaponEnchantments,
|
||||||
|
0x0008u);
|
||||||
|
if (damageStyle == ItemAppraisalFontStyle.Normal)
|
||||||
|
{
|
||||||
|
damageStyle = EnchantmentStyle(
|
||||||
|
appraisal.WeaponEnchantments,
|
||||||
|
0x0010u);
|
||||||
|
}
|
||||||
|
report.Line($"{damageLabel}: {damage}", damageStyle);
|
||||||
|
|
||||||
int elementalBonus = properties.GetInt(204u);
|
int elementalBonus = properties.GetInt(204u);
|
||||||
if (elementalBonus > 0)
|
if (elementalBonus > 0)
|
||||||
|
|
@ -202,7 +221,10 @@ public static class ItemAppraisalTextFormatter
|
||||||
|
|
||||||
if (launcher)
|
if (launcher)
|
||||||
report.Line(
|
report.Line(
|
||||||
$"Damage Modifier: {FormatModifier(weapon.DamageMod)}.");
|
$"Damage Modifier: {FormatModifier(weapon.DamageMod)}.",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.WeaponEnchantments,
|
||||||
|
0x0020u));
|
||||||
|
|
||||||
const uint timedWeaponLocations =
|
const uint timedWeaponLocations =
|
||||||
(uint)(EquipMask.MeleeWeapon
|
(uint)(EquipMask.MeleeWeapon
|
||||||
|
|
@ -211,7 +233,10 @@ public static class ItemAppraisalTextFormatter
|
||||||
if ((validLocations & timedWeaponLocations) != 0)
|
if ((validLocations & timedWeaponLocations) != 0)
|
||||||
report.Line(
|
report.Line(
|
||||||
$"Speed: {WeaponTimeName((int)weapon.WeaponTime)} "
|
$"Speed: {WeaponTimeName((int)weapon.WeaponTime)} "
|
||||||
+ $"({weapon.WeaponTime.ToString(CultureInfo.CurrentCulture)})");
|
+ $"({weapon.WeaponTime.ToString(CultureInfo.CurrentCulture)})",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.WeaponEnchantments,
|
||||||
|
0x0004u));
|
||||||
|
|
||||||
if (launcher && weapon.MaxVelocity > 0d)
|
if (launcher && weapon.MaxVelocity > 0d)
|
||||||
{
|
{
|
||||||
|
|
@ -235,7 +260,10 @@ public static class ItemAppraisalTextFormatter
|
||||||
if (!launcher && Math.Abs(weapon.WeaponOffense - 1d) > 0.000001d)
|
if (!launcher && Math.Abs(weapon.WeaponOffense - 1d) > 0.000001d)
|
||||||
report.Line(
|
report.Line(
|
||||||
$"Bonus to Attack Skill: "
|
$"Bonus to Attack Skill: "
|
||||||
+ $"{FormatSignedPercent(weapon.WeaponOffense - 1d)}.");
|
+ $"{FormatSignedPercent(weapon.WeaponOffense - 1d)}.",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.WeaponEnchantments,
|
||||||
|
0x0001u));
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowAmmunitionDescription(report, validLocations, ammoType);
|
ShowAmmunitionDescription(report, validLocations, ammoType);
|
||||||
|
|
@ -247,15 +275,32 @@ public static class ItemAppraisalTextFormatter
|
||||||
report.Line(
|
report.Line(
|
||||||
appraisal.Success
|
appraisal.Success
|
||||||
? $"Armor Level: {armorLevel.ToString(CultureInfo.CurrentCulture)}"
|
? $"Armor Level: {armorLevel.ToString(CultureInfo.CurrentCulture)}"
|
||||||
: "Armor Level: Unknown");
|
: "Armor Level: Unknown",
|
||||||
ShowProtection(report, "Slashing", armorLevel, armor.SlashingProtection);
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0001u));
|
||||||
ShowProtection(report, "Piercing", armorLevel, armor.PiercingProtection);
|
ShowProtection(
|
||||||
ShowProtection(report, "Bludgeoning", armorLevel, armor.BludgeoningProtection);
|
report, "Slashing", armorLevel, armor.SlashingProtection,
|
||||||
ShowProtection(report, "Fire", armorLevel, armor.FireProtection);
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0002u));
|
||||||
ShowProtection(report, "Cold", armorLevel, armor.ColdProtection);
|
ShowProtection(
|
||||||
ShowProtection(report, "Acid", armorLevel, armor.AcidProtection);
|
report, "Piercing", armorLevel, armor.PiercingProtection,
|
||||||
ShowProtection(report, "Electric", armorLevel, armor.LightningProtection);
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0004u));
|
||||||
ShowProtection(report, "Nether", armorLevel, armor.NetherProtection);
|
ShowProtection(
|
||||||
|
report, "Bludgeoning", armorLevel, armor.BludgeoningProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0008u));
|
||||||
|
ShowProtection(
|
||||||
|
report, "Fire", armorLevel, armor.FireProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0020u));
|
||||||
|
ShowProtection(
|
||||||
|
report, "Cold", armorLevel, armor.ColdProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0010u));
|
||||||
|
ShowProtection(
|
||||||
|
report, "Acid", armorLevel, armor.AcidProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0040u));
|
||||||
|
ShowProtection(
|
||||||
|
report, "Electric", armorLevel, armor.LightningProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0080u));
|
||||||
|
ShowProtection(
|
||||||
|
report, "Nether", armorLevel, armor.NetherProtection,
|
||||||
|
EnchantmentStyle(appraisal.ArmorEnchantments, 0x0100u));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowAmmunitionDescription(
|
private static void ShowAmmunitionDescription(
|
||||||
|
|
@ -292,7 +337,8 @@ public static class ItemAppraisalTextFormatter
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
string damageType,
|
string damageType,
|
||||||
int armorLevel,
|
int armorLevel,
|
||||||
float modifier)
|
float modifier,
|
||||||
|
ItemAppraisalFontStyle style)
|
||||||
{
|
{
|
||||||
// AppraisalSystem::DamageResistanceToString @ 0x005B5490.
|
// AppraisalSystem::DamageResistanceToString @ 0x005B5490.
|
||||||
string quality = modifier switch
|
string quality = modifier switch
|
||||||
|
|
@ -308,15 +354,22 @@ public static class ItemAppraisalTextFormatter
|
||||||
double effective = armorLevel * modifier;
|
double effective = armorLevel * modifier;
|
||||||
report.Line(
|
report.Line(
|
||||||
$"{damageType}: {quality} "
|
$"{damageType}: {quality} "
|
||||||
+ $"({effective.ToString("0", CultureInfo.CurrentCulture)})");
|
+ $"({effective.ToString("0", CultureInfo.CurrentCulture)})",
|
||||||
|
style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowDefenseModData @ 0x004B1DF0</c>.</summary>
|
/// <summary><c>Appraisal_ShowDefenseModData @ 0x004B1DF0</c>.</summary>
|
||||||
private static void ShowDefenseModifiers(
|
private static void ShowDefenseModifiers(
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
PropertyBundle properties)
|
AppraiseInfoParser.Parsed appraisal)
|
||||||
{
|
{
|
||||||
ShowModifier(report, properties, 29u, "Bonus to Melee Defense");
|
PropertyBundle properties = appraisal.Properties;
|
||||||
|
ShowModifier(
|
||||||
|
report,
|
||||||
|
properties,
|
||||||
|
29u,
|
||||||
|
"Bonus to Melee Defense",
|
||||||
|
EnchantmentStyle(appraisal.WeaponEnchantments, 0x0002u));
|
||||||
ShowModifier(report, properties, 149u, "Bonus to Missile Defense");
|
ShowModifier(report, properties, 149u, "Bonus to Missile Defense");
|
||||||
ShowModifier(report, properties, 150u, "Bonus to Magic Defense");
|
ShowModifier(report, properties, 150u, "Bonus to Magic Defense");
|
||||||
}
|
}
|
||||||
|
|
@ -325,12 +378,15 @@ public static class ItemAppraisalTextFormatter
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
PropertyBundle properties,
|
PropertyBundle properties,
|
||||||
uint property,
|
uint property,
|
||||||
string label)
|
string label,
|
||||||
|
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
|
||||||
{
|
{
|
||||||
if (!properties.Floats.TryGetValue(property, out double modifier)
|
if (!properties.Floats.TryGetValue(property, out double modifier)
|
||||||
|| Math.Abs(modifier - 1d) <= 0.000001d)
|
|| Math.Abs(modifier - 1d) <= 0.000001d)
|
||||||
return;
|
return;
|
||||||
report.Line($"{label}: {FormatSignedPercent(modifier - 1d)}.");
|
report.Line(
|
||||||
|
$"{label}: {FormatSignedPercent(modifier - 1d)}.",
|
||||||
|
style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowShortMagicInfo @ 0x004B2C90</c>.</summary>
|
/// <summary><c>Appraisal_ShowShortMagicInfo @ 0x004B2C90</c>.</summary>
|
||||||
|
|
@ -343,7 +399,7 @@ public static class ItemAppraisalTextFormatter
|
||||||
return;
|
return;
|
||||||
if (!appraisal.Success)
|
if (!appraisal.Success)
|
||||||
{
|
{
|
||||||
report.Line("Spells: unknown.");
|
report.Paragraph("Spells: unknown.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,7 +411,7 @@ public static class ItemAppraisalTextFormatter
|
||||||
return resolveSpell(id)?.Name
|
return resolveSpell(id)?.Name
|
||||||
?? $"Spell {id.ToString(CultureInfo.CurrentCulture)}";
|
?? $"Spell {id.ToString(CultureInfo.CurrentCulture)}";
|
||||||
}));
|
}));
|
||||||
report.Line($"Spells: {names}");
|
report.Paragraph($"Spells: {names}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowSpecialProperties @ 0x004B0140</c>.</summary>
|
/// <summary><c>Appraisal_ShowSpecialProperties @ 0x004B0140</c>.</summary>
|
||||||
|
|
@ -491,22 +547,22 @@ public static class ItemAppraisalTextFormatter
|
||||||
int minimum = properties.GetInt(86u);
|
int minimum = properties.GetInt(86u);
|
||||||
int maximum = properties.GetInt(87u);
|
int maximum = properties.GetInt(87u);
|
||||||
if (minimum > 0 && maximum > 0)
|
if (minimum > 0 && maximum > 0)
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Restricted to characters of level "
|
$"Restricted to characters of level "
|
||||||
+ $"{minimum.ToString(CultureInfo.CurrentCulture)} to "
|
+ $"{minimum.ToString(CultureInfo.CurrentCulture)} to "
|
||||||
+ $"{maximum.ToString(CultureInfo.CurrentCulture)}.");
|
+ $"{maximum.ToString(CultureInfo.CurrentCulture)}.");
|
||||||
else if (minimum > 0)
|
else if (minimum > 0)
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Restricted to characters of level "
|
$"Restricted to characters of level "
|
||||||
+ $"{minimum.ToString(CultureInfo.CurrentCulture)} or greater.");
|
+ $"{minimum.ToString(CultureInfo.CurrentCulture)} or greater.");
|
||||||
else if (maximum > 0)
|
else if (maximum > 0)
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Restricted to characters of level "
|
$"Restricted to characters of level "
|
||||||
+ $"{maximum.ToString(CultureInfo.CurrentCulture)} or lower.");
|
+ $"{maximum.ToString(CultureInfo.CurrentCulture)} or lower.");
|
||||||
|
|
||||||
string destination = properties.GetString(38u);
|
string destination = properties.GetString(38u);
|
||||||
if (!string.IsNullOrWhiteSpace(destination))
|
if (!string.IsNullOrWhiteSpace(destination))
|
||||||
report.Line($"Destination: {destination}");
|
report.Paragraph($"Destination: {destination}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowWieldRequirements @ 0x004AF9A0</c>.</summary>
|
/// <summary><c>Appraisal_ShowWieldRequirements @ 0x004AF9A0</c>.</summary>
|
||||||
|
|
@ -685,18 +741,25 @@ public static class ItemAppraisalTextFormatter
|
||||||
/// <summary><c>Appraisal_ShowCasterData @ 0x004B1B10</c>.</summary>
|
/// <summary><c>Appraisal_ShowCasterData @ 0x004B1B10</c>.</summary>
|
||||||
private static void ShowCasterData(
|
private static void ShowCasterData(
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
PropertyBundle properties)
|
AppraiseInfoParser.Parsed appraisal)
|
||||||
{
|
{
|
||||||
|
PropertyBundle properties = appraisal.Properties;
|
||||||
if (properties.Floats.TryGetValue(144u, out double manaConversion))
|
if (properties.Floats.TryGetValue(144u, out double manaConversion))
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Bonus to Mana Conversion: "
|
$"Bonus to Mana Conversion: "
|
||||||
+ $"{FormatSignedPercent(manaConversion)}.");
|
+ $"{FormatSignedPercent(manaConversion)}.",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.ResistEnchantments,
|
||||||
|
0x1000u));
|
||||||
|
|
||||||
if (properties.Floats.TryGetValue(152u, out double elemental)
|
if (properties.Floats.TryGetValue(152u, out double elemental)
|
||||||
&& properties.Ints.TryGetValue(45u, out int damageType))
|
&& properties.Ints.TryGetValue(45u, out int damageType))
|
||||||
{
|
{
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Damage bonus for {DamageTypeName((uint)damageType)} spells:");
|
$"Damage bonus for {DamageTypeName((uint)damageType)} spells:",
|
||||||
|
EnchantmentStyle(
|
||||||
|
appraisal.ResistEnchantments,
|
||||||
|
0x2000u));
|
||||||
report.Line($" vs. Monsters: {FormatSignedPercent(elemental - 1d)}.");
|
report.Line($" vs. Monsters: {FormatSignedPercent(elemental - 1d)}.");
|
||||||
double playerModifier = 1d + (elemental - 1d) * 0.25d;
|
double playerModifier = 1d + (elemental - 1d) * 0.25d;
|
||||||
report.Line(
|
report.Line(
|
||||||
|
|
@ -721,13 +784,13 @@ public static class ItemAppraisalTextFormatter
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
if (boost != 0 && vital is not null)
|
if (boost != 0 && vital is not null)
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"{(boost > 0 ? "Restores" : "Depletes")} "
|
$"{(boost > 0 ? "Restores" : "Depletes")} "
|
||||||
+ $"{Math.Abs(boost).ToString(CultureInfo.CurrentCulture)} "
|
+ $"{Math.Abs(boost).ToString(CultureInfo.CurrentCulture)} "
|
||||||
+ $"{vital} when used.");
|
+ $"{vital} when used.");
|
||||||
|
|
||||||
if (boost != 0 && properties.Floats.ContainsKey(100u))
|
if (boost != 0 && properties.Floats.ContainsKey(100u))
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Bonus to Healing Skill: {boost.ToString(CultureInfo.CurrentCulture)}");
|
$"Bonus to Healing Skill: {boost.ToString(CultureInfo.CurrentCulture)}");
|
||||||
if (properties.Floats.TryGetValue(100u, out double healKitModifier))
|
if (properties.Floats.TryGetValue(100u, out double healKitModifier))
|
||||||
report.Line(
|
report.Line(
|
||||||
|
|
@ -742,36 +805,73 @@ public static class ItemAppraisalTextFormatter
|
||||||
private static void ShowCapacityAndLock(
|
private static void ShowCapacityAndLock(
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
ClientObject obj,
|
ClientObject obj,
|
||||||
PropertyBundle properties)
|
AppraiseInfoParser.Parsed appraisal)
|
||||||
{
|
{
|
||||||
if (obj.ItemsCapacity > 0 && obj.ContainersCapacity > 0)
|
PropertyBundle properties = appraisal.Properties;
|
||||||
report.Line(
|
// Appraisal_ShowCapacity @ 0x004B2680 suppresses the hook object's
|
||||||
$"Can hold up to {obj.ItemsCapacity.ToString(CultureInfo.CurrentCulture)} "
|
// own container capacity while a HookProfile describes the item
|
||||||
+ $"items and {obj.ContainersCapacity.ToString(CultureInfo.CurrentCulture)} containers.");
|
// mounted on it. This is the Black Phyntos Hive 255/255 case.
|
||||||
else if (obj.ItemsCapacity > 0)
|
if (!obj.IsHook || appraisal.HookProfile is null)
|
||||||
report.Line(
|
{
|
||||||
$"Can hold up to {obj.ItemsCapacity.ToString(CultureInfo.CurrentCulture)} items.");
|
if (obj.ItemsCapacity > 0 && obj.ContainersCapacity > 0)
|
||||||
else if (obj.ContainersCapacity > 0)
|
report.Paragraph(
|
||||||
report.Line(
|
$"Can hold up to {obj.ItemsCapacity.ToString(CultureInfo.CurrentCulture)} "
|
||||||
$"Can hold up to {obj.ContainersCapacity.ToString(CultureInfo.CurrentCulture)} containers.");
|
+ $"items and {obj.ContainersCapacity.ToString(CultureInfo.CurrentCulture)} containers.");
|
||||||
|
else if (obj.ItemsCapacity > 0)
|
||||||
|
report.Paragraph(
|
||||||
|
$"Can hold up to {obj.ItemsCapacity.ToString(CultureInfo.CurrentCulture)} items.");
|
||||||
|
else if (obj.ContainersCapacity > 0)
|
||||||
|
report.Paragraph(
|
||||||
|
$"Can hold up to {obj.ContainersCapacity.ToString(CultureInfo.CurrentCulture)} containers.");
|
||||||
|
|
||||||
int pages = properties.GetInt(174u);
|
int pages = properties.GetInt(175u);
|
||||||
int pagesUsed = properties.GetInt(175u);
|
int pagesUsed = properties.GetInt(174u);
|
||||||
if (pages > 0)
|
if (pages > 0)
|
||||||
report.Line(
|
report.Paragraph(
|
||||||
$"Pages: {pagesUsed.ToString(CultureInfo.CurrentCulture)} / "
|
$"{pagesUsed.ToString(CultureInfo.CurrentCulture)} of "
|
||||||
+ $"{pages.ToString(CultureInfo.CurrentCulture)}");
|
+ $"{pages.ToString(CultureInfo.CurrentCulture)} pages full.");
|
||||||
|
}
|
||||||
|
|
||||||
if (properties.GetBool(3u))
|
// Appraisal_ShowLockAppraiseInfo @ 0x004B2790 does not present lock
|
||||||
report.Line("This item is locked.");
|
// data for hook objects.
|
||||||
int resistance = properties.GetInt(38u);
|
if (obj.IsHook)
|
||||||
if (resistance > 0)
|
return;
|
||||||
report.Line(
|
|
||||||
$"Resistance to Lockpick: {resistance.ToString(CultureInfo.CurrentCulture)}");
|
bool hasLocked = properties.Bools.TryGetValue(3u, out bool locked);
|
||||||
int chance = properties.GetInt(173u);
|
bool hasResistance = properties.Ints.TryGetValue(
|
||||||
if (chance > 0)
|
38u,
|
||||||
report.Line(
|
out int resistance);
|
||||||
$"Chance of success: {chance.ToString(CultureInfo.CurrentCulture)}%");
|
if (!hasLocked)
|
||||||
|
{
|
||||||
|
if (hasResistance && resistance != 0)
|
||||||
|
{
|
||||||
|
report.Paragraph(
|
||||||
|
$"Bonus to Lockpick Skill: "
|
||||||
|
+ $"{resistance.ToString("+0;-0;0", CultureInfo.CurrentCulture)}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locked)
|
||||||
|
{
|
||||||
|
report.Paragraph("Unlocked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Paragraph("Locked");
|
||||||
|
if (!hasResistance)
|
||||||
|
{
|
||||||
|
report.Paragraph("You can't tell how hard the lock is to pick.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.Ints.TryGetValue(173u, out int chance)
|
||||||
|
&& LockpickDifficulty(chance) is { } difficulty)
|
||||||
|
{
|
||||||
|
report.Paragraph(
|
||||||
|
$"The lock looks {difficulty} to pick "
|
||||||
|
+ $"(Resistance {resistance.ToString(CultureInfo.CurrentCulture)}).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowManaStoneInfo @ 0x004B3900</c>.</summary>
|
/// <summary><c>Appraisal_ShowManaStoneInfo @ 0x004B3900</c>.</summary>
|
||||||
|
|
@ -808,7 +908,7 @@ public static class ItemAppraisalTextFormatter
|
||||||
|
|
||||||
if (properties.GetBool(63u))
|
if (properties.GetBool(63u))
|
||||||
{
|
{
|
||||||
report.Line("Number of uses remaining: Unlimited");
|
report.Line("Number of uses remaining: Unlimited");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -820,7 +920,7 @@ public static class ItemAppraisalTextFormatter
|
||||||
&& (obj.MaxStructure != 0
|
&& (obj.MaxStructure != 0
|
||||||
|| appraisal.HookProfile is { Flags: var flags }
|
|| appraisal.HookProfile is { Flags: var flags }
|
||||||
&& (flags & 0xAu) != 0))
|
&& (flags & 0xAu) != 0))
|
||||||
report.Line("Number of uses remaining: Unknown");
|
report.Paragraph("Number of uses remaining: Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowCraftsman(
|
private static void ShowCraftsman(
|
||||||
|
|
@ -842,7 +942,7 @@ public static class ItemAppraisalTextFormatter
|
||||||
report.Line("This item cannot be sold.");
|
report.Line("This item cannot be sold.");
|
||||||
int rare = properties.GetInt(17u);
|
int rare = properties.GetInt(17u);
|
||||||
if (rare > 0)
|
if (rare > 0)
|
||||||
report.Line($"Rare #{rare.ToString(CultureInfo.CurrentCulture)}");
|
report.Paragraph($"Rare #{rare.ToString(CultureInfo.CurrentCulture)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>Appraisal_ShowMagicInfo @ 0x004B2E10</c>.</summary>
|
/// <summary><c>Appraisal_ShowMagicInfo @ 0x004B2E10</c>.</summary>
|
||||||
|
|
@ -888,24 +988,46 @@ public static class ItemAppraisalTextFormatter
|
||||||
}
|
}
|
||||||
else if (properties.Ints.TryGetValue(117u, out int manaCost))
|
else if (properties.Ints.TryGetValue(117u, out int manaCost))
|
||||||
{
|
{
|
||||||
report.Line($"Mana Cost: {manaCost.ToString(CultureInfo.CurrentCulture)}.");
|
string manaCostText =
|
||||||
|
$"Mana Cost: {manaCost.ToString(CultureInfo.CurrentCulture)}.";
|
||||||
if (manaCost > 0)
|
if (manaCost > 0)
|
||||||
report.Line("(Can be reduced by the Mana Conversion skill).");
|
{
|
||||||
|
manaCostText +=
|
||||||
|
"\n(Can be reduced by the Mana Conversion skill).";
|
||||||
|
}
|
||||||
|
report.Paragraph(manaCostText);
|
||||||
}
|
}
|
||||||
|
|
||||||
report.Paragraph("Spell Descriptions:");
|
report.Paragraph(BuildSpellDescriptionBlock(
|
||||||
foreach ((uint id, SpellMetadata? metadata) in ordinary)
|
"Spell Descriptions:",
|
||||||
report.Spell(metadata?.Name, metadata?.Description, id);
|
ordinary));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enchantments.Count != 0)
|
if (enchantments.Count != 0)
|
||||||
{
|
{
|
||||||
report.Paragraph("Enchantments:");
|
report.Paragraph(BuildSpellDescriptionBlock(
|
||||||
foreach ((uint id, SpellMetadata? metadata) in enchantments)
|
"Enchantments:",
|
||||||
report.Spell(metadata?.Name, metadata?.Description, id);
|
enchantments));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildSpellDescriptionBlock(
|
||||||
|
string heading,
|
||||||
|
IReadOnlyList<(uint Id, SpellMetadata? Metadata)> spells)
|
||||||
|
{
|
||||||
|
var text = new StringBuilder(heading);
|
||||||
|
foreach ((uint id, SpellMetadata? metadata) in spells)
|
||||||
|
{
|
||||||
|
string name = !string.IsNullOrWhiteSpace(metadata?.Name)
|
||||||
|
? metadata.Name
|
||||||
|
: $"Spell {id.ToString(CultureInfo.CurrentCulture)}";
|
||||||
|
text.Append('\n').Append(name);
|
||||||
|
if (!string.IsNullOrWhiteSpace(metadata?.Description))
|
||||||
|
text.Append('\n').Append(metadata.Description);
|
||||||
|
}
|
||||||
|
return text.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private static void ShowDescription(
|
private static void ShowDescription(
|
||||||
RetailReportBuilder report,
|
RetailReportBuilder report,
|
||||||
PropertyBundle properties)
|
PropertyBundle properties)
|
||||||
|
|
@ -979,6 +1101,47 @@ public static class ItemAppraisalTextFormatter
|
||||||
private static string FormatSignedPercent(double modifier)
|
private static string FormatSignedPercent(double modifier)
|
||||||
=> modifier.ToString("+0%;-0%;0%", CultureInfo.CurrentCulture);
|
=> modifier.ToString("+0%;-0%;0%", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>AppraisalSystem::LockpickSuccessPercentToString
|
||||||
|
/// @ 0x005B4600</c>.
|
||||||
|
/// </summary>
|
||||||
|
private static string? LockpickDifficulty(int successPercent)
|
||||||
|
=> successPercent switch
|
||||||
|
{
|
||||||
|
< 0 => null,
|
||||||
|
0 => "impossible",
|
||||||
|
< 5 => "ridiculously difficult",
|
||||||
|
< 15 => "extremely difficult",
|
||||||
|
< 35 => "quite difficult",
|
||||||
|
< 50 => "difficult",
|
||||||
|
< 70 => "challenging",
|
||||||
|
< 85 => "mildly challenging",
|
||||||
|
< 95 => "easy",
|
||||||
|
_ => "trivial",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>AppraisalProfile::InqIntEnchantmentMod @ 0x005B2C10</c>
|
||||||
|
/// and <c>InqFloatEnchantmentMod @ 0x005B2C70</c>. The wire carries the
|
||||||
|
/// low/high halves as two consecutive u16 values: the low bit marks an
|
||||||
|
/// enchanted value and the corresponding bit sixteen positions higher
|
||||||
|
/// selects the beneficial color.
|
||||||
|
/// </summary>
|
||||||
|
private static ItemAppraisalFontStyle EnchantmentStyle(
|
||||||
|
(ushort Highlight, ushort Color)? encoded,
|
||||||
|
uint lowBit)
|
||||||
|
{
|
||||||
|
if (encoded is not { } halves)
|
||||||
|
return ItemAppraisalFontStyle.Normal;
|
||||||
|
|
||||||
|
uint bitfield = halves.Highlight | ((uint)halves.Color << 16);
|
||||||
|
if ((bitfield & lowBit) == 0u)
|
||||||
|
return ItemAppraisalFontStyle.Normal;
|
||||||
|
return (bitfield & (lowBit << 16)) != 0u
|
||||||
|
? ItemAppraisalFontStyle.Beneficial
|
||||||
|
: ItemAppraisalFontStyle.Detrimental;
|
||||||
|
}
|
||||||
|
|
||||||
private static string DamageTypeName(uint type) => type switch
|
private static string DamageTypeName(uint type) => type switch
|
||||||
{
|
{
|
||||||
1u => "Slashing",
|
1u => "Slashing",
|
||||||
|
|
@ -1093,39 +1256,18 @@ public static class ItemAppraisalTextFormatter
|
||||||
|
|
||||||
private sealed class RetailReportBuilder
|
private sealed class RetailReportBuilder
|
||||||
{
|
{
|
||||||
private readonly StringBuilder _text = new();
|
private readonly ItemAppraisalReportBuilder _report = new();
|
||||||
|
|
||||||
public bool IsEmpty => _text.Length == 0;
|
public void Line(
|
||||||
|
string value,
|
||||||
|
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
|
||||||
|
=> _report.Line(value, style);
|
||||||
|
|
||||||
public void Line(string value)
|
public void Paragraph(
|
||||||
{
|
string value,
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
ItemAppraisalFontStyle style = ItemAppraisalFontStyle.Normal)
|
||||||
return;
|
=> _report.Paragraph(value, style);
|
||||||
if (_text.Length != 0 && _text[^1] != '\n')
|
|
||||||
_text.Append('\n');
|
|
||||||
_text.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Paragraph(string value)
|
public ItemAppraisalReport Build() => _report.Build();
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
return;
|
|
||||||
if (_text.Length != 0)
|
|
||||||
_text.Append("\n\n");
|
|
||||||
_text.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Spell(string? name, string? description, uint spellId)
|
|
||||||
{
|
|
||||||
string resolvedName = !string.IsNullOrWhiteSpace(name)
|
|
||||||
? name
|
|
||||||
: $"Spell {spellId.ToString(CultureInfo.CurrentCulture)}";
|
|
||||||
Paragraph(
|
|
||||||
string.IsNullOrWhiteSpace(description)
|
|
||||||
? resolvedName
|
|
||||||
: $"{resolvedName}\n{description}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => _text.ToString().TrimEnd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,15 @@ public sealed class UiText : UiElement, IUiDatStateful
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector4 DefaultColor { get; set; } = Vector4.One;
|
public Vector4 DefaultColor { get; set; } = Vector4.One;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authored <c>UIElement_Text</c> font-color list from LayoutDesc property
|
||||||
|
/// <c>0x1B</c>. Retail <c>AppendTextWithFont @ 0x00469D70</c> selects an
|
||||||
|
/// entry by index for every appended fragment. Controllers that port that
|
||||||
|
/// API use this palette instead of hard-coded colors.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Vector4> FontColorPalette { get; set; }
|
||||||
|
= Array.Empty<Vector4>();
|
||||||
|
|
||||||
/// <summary>Backing fill behind the text. Defaults to transparent so an unbound
|
/// <summary>Backing fill behind the text. Defaults to transparent so an unbound
|
||||||
/// UiText (no controller) draws nothing. Set to the retail translucent value by
|
/// UiText (no controller) draws nothing. Set to the retail translucent value by
|
||||||
/// the controller (e.g. <c>ChatWindowController</c>).</summary>
|
/// the controller (e.g. <c>ChatWindowController</c>).</summary>
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,11 @@ public static class CreateObject
|
||||||
int? Burden = null,
|
int? Burden = null,
|
||||||
int? ItemsCapacity = null,
|
int? ItemsCapacity = null,
|
||||||
int? ContainersCapacity = null,
|
int? ContainersCapacity = null,
|
||||||
|
// PublicWeenieDesc hook identity. ACCWeenieObject::IsHook
|
||||||
|
// @ 0x0058C660 requires both values to be non-zero.
|
||||||
|
// The wire stores HookItemTypes as u32 and HookType as u16.
|
||||||
|
uint? HookItemTypes = null,
|
||||||
|
uint? HookType = null,
|
||||||
uint? ContainerId = null,
|
uint? ContainerId = null,
|
||||||
uint? WielderId = null,
|
uint? WielderId = null,
|
||||||
uint? ValidLocations = null,
|
uint? ValidLocations = null,
|
||||||
|
|
@ -778,6 +783,8 @@ public static class CreateObject
|
||||||
int? wBurden = null;
|
int? wBurden = null;
|
||||||
int? wItemsCapacity = null;
|
int? wItemsCapacity = null;
|
||||||
int? wContainersCapacity = null;
|
int? wContainersCapacity = null;
|
||||||
|
uint? wHookItemTypes = null;
|
||||||
|
uint? wHookType = null;
|
||||||
uint? wContainerId = null;
|
uint? wContainerId = null;
|
||||||
uint? wWielderId = null;
|
uint? wWielderId = null;
|
||||||
uint? wValidLocations = null;
|
uint? wValidLocations = null;
|
||||||
|
|
@ -852,9 +859,9 @@ public static class CreateObject
|
||||||
// 0x00400000 Spell u16 (skip)
|
// 0x00400000 Spell u16 (skip)
|
||||||
// 0x02000000 HouseOwner u32 (skip)
|
// 0x02000000 HouseOwner u32 (skip)
|
||||||
// 0x04000000 HouseRestrictions RestrictionDB (skip, variable-length)
|
// 0x04000000 HouseRestrictions RestrictionDB (skip, variable-length)
|
||||||
// 0x20000000 HookItemTypes u32 (skip)
|
// 0x20000000 HookItemTypes u32 CAPTURE
|
||||||
// 0x00000040 Monarch u32 (skip)
|
// 0x00000040 Monarch u32 (skip)
|
||||||
// 0x10000000 HookType u16 (skip)
|
// 0x10000000 HookType u16 CAPTURE
|
||||||
// 0x40000000 IconOverlay PackedDwordKnownType(0x06000000) CAPTURE
|
// 0x40000000 IconOverlay PackedDwordKnownType(0x06000000) CAPTURE
|
||||||
// weenieFlags2 bit 0x01:
|
// weenieFlags2 bit 0x01:
|
||||||
// IconUnderlay PackedDwordKnownType(0x06000000) CAPTURE
|
// IconUnderlay PackedDwordKnownType(0x06000000) CAPTURE
|
||||||
|
|
@ -1050,7 +1057,7 @@ public static class CreateObject
|
||||||
if ((weenieFlags & 0x20000000u) != 0) // HookItemTypes u32
|
if ((weenieFlags & 0x20000000u) != 0) // HookItemTypes u32
|
||||||
{
|
{
|
||||||
if (body.Length - pos < 4) throw new FormatException("trunc HookItemTypes");
|
if (body.Length - pos < 4) throw new FormatException("trunc HookItemTypes");
|
||||||
pos += 4;
|
wHookItemTypes = ReadU32(body, ref pos);
|
||||||
}
|
}
|
||||||
if ((weenieFlags & 0x00000040u) != 0) // Monarch u32
|
if ((weenieFlags & 0x00000040u) != 0) // Monarch u32
|
||||||
{
|
{
|
||||||
|
|
@ -1060,6 +1067,7 @@ public static class CreateObject
|
||||||
if ((weenieFlags & 0x10000000u) != 0) // HookType u16
|
if ((weenieFlags & 0x10000000u) != 0) // HookType u16
|
||||||
{
|
{
|
||||||
if (body.Length - pos < 2) throw new FormatException("trunc HookType");
|
if (body.Length - pos < 2) throw new FormatException("trunc HookType");
|
||||||
|
wHookType = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||||
pos += 2;
|
pos += 2;
|
||||||
}
|
}
|
||||||
if ((weenieFlags & 0x40000000u) != 0) // IconOverlay PackedDwordOfKnownType(0x06000000) ← CAPTURE
|
if ((weenieFlags & 0x40000000u) != 0) // IconOverlay PackedDwordOfKnownType(0x06000000) ← CAPTURE
|
||||||
|
|
@ -1117,6 +1125,7 @@ public static class CreateObject
|
||||||
WeenieClassId: weenieClassId,
|
WeenieClassId: weenieClassId,
|
||||||
Value: wValue, StackSize: wStackSize, StackSizeMax: wMaxStackSize,
|
Value: wValue, StackSize: wStackSize, StackSizeMax: wMaxStackSize,
|
||||||
Burden: wBurden, ItemsCapacity: wItemsCapacity, ContainersCapacity: wContainersCapacity,
|
Burden: wBurden, ItemsCapacity: wItemsCapacity, ContainersCapacity: wContainersCapacity,
|
||||||
|
HookItemTypes: wHookItemTypes, HookType: wHookType,
|
||||||
ContainerId: wContainerId, WielderId: wWielderId,
|
ContainerId: wContainerId, WielderId: wWielderId,
|
||||||
ValidLocations: wValidLocations, CurrentWieldedLocation: wCurrentWieldedLocation,
|
ValidLocations: wValidLocations, CurrentWieldedLocation: wCurrentWieldedLocation,
|
||||||
Priority: wPriority, Structure: wStructure, MaxStructure: wMaxStructure,
|
Priority: wPriority, Structure: wStructure, MaxStructure: wMaxStructure,
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,8 @@ public static class ObjectTableWiring
|
||||||
Priority: s.Priority,
|
Priority: s.Priority,
|
||||||
ItemsCapacity: s.ItemsCapacity,
|
ItemsCapacity: s.ItemsCapacity,
|
||||||
ContainersCapacity: s.ContainersCapacity,
|
ContainersCapacity: s.ContainersCapacity,
|
||||||
|
HookItemTypes: s.HookItemTypes,
|
||||||
|
HookType: s.HookType,
|
||||||
Structure: s.Structure,
|
Structure: s.Structure,
|
||||||
MaxStructure: s.MaxStructure,
|
MaxStructure: s.MaxStructure,
|
||||||
Workmanship: s.Workmanship,
|
Workmanship: s.Workmanship,
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,9 @@ public sealed class WorldSession : IDisposable
|
||||||
uint? SpellId = null,
|
uint? SpellId = null,
|
||||||
uint? CooldownId = null,
|
uint? CooldownId = null,
|
||||||
double? CooldownDuration = null,
|
double? CooldownDuration = null,
|
||||||
PhysicsSpawnData? Physics = null);
|
PhysicsSpawnData? Physics = null,
|
||||||
|
uint? HookItemTypes = null,
|
||||||
|
uint? HookType = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Projects the wire-level CreateObject result into the stable session
|
/// Projects the wire-level CreateObject result into the stable session
|
||||||
|
|
@ -219,7 +221,9 @@ public sealed class WorldSession : IDisposable
|
||||||
SpellId: parsed.SpellId,
|
SpellId: parsed.SpellId,
|
||||||
CooldownId: parsed.CooldownId,
|
CooldownId: parsed.CooldownId,
|
||||||
CooldownDuration: parsed.CooldownDuration,
|
CooldownDuration: parsed.CooldownDuration,
|
||||||
Physics: parsed.Physics);
|
Physics: parsed.Physics,
|
||||||
|
HookItemTypes: parsed.HookItemTypes,
|
||||||
|
HookType: parsed.HookType);
|
||||||
|
|
||||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||||
public event Action<EntitySpawn>? EntitySpawned;
|
public event Action<EntitySpawn>? EntitySpawned;
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,15 @@ public sealed class ClientObject
|
||||||
public uint WielderId { get; set; } // PropertyInstanceId.Wielder; 0 = not wielded
|
public uint WielderId { get; set; } // PropertyInstanceId.Wielder; 0 = not wielded
|
||||||
public int ItemsCapacity { get; set; } // main-pack slots (containers)
|
public int ItemsCapacity { get; set; } // main-pack slots (containers)
|
||||||
public int ContainersCapacity{ get; set; } // side-pack slots (containers)
|
public int ContainersCapacity{ get; set; } // side-pack slots (containers)
|
||||||
|
/// <summary>Retail <c>PublicWeenieDesc._hook_item_types</c>.</summary>
|
||||||
|
public uint HookItemTypes { get; set; }
|
||||||
|
/// <summary>Retail <c>PublicWeenieDesc._hook_type</c>.</summary>
|
||||||
|
public uint HookType { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Port of <c>ACCWeenieObject::IsHook @ 0x0058C660</c>.
|
||||||
|
/// A hook must publish both a hook type and an accepted item-type mask.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHook => HookType != 0u && HookItemTypes != 0u;
|
||||||
public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order
|
public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order
|
||||||
public uint? Useability { get; set; } // ITEM_USEABLE from PublicWeenieDesc
|
public uint? Useability { get; set; } // ITEM_USEABLE from PublicWeenieDesc
|
||||||
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
||||||
|
|
@ -274,7 +283,9 @@ public readonly record struct WeenieData(
|
||||||
ushort? AmmoType = null,
|
ushort? AmmoType = null,
|
||||||
uint? SpellId = null,
|
uint? SpellId = null,
|
||||||
uint? CooldownId = null,
|
uint? CooldownId = null,
|
||||||
double? CooldownDuration = null);
|
double? CooldownDuration = null,
|
||||||
|
uint? HookItemTypes = null,
|
||||||
|
uint? HookType = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@ public sealed class ClientObjectTable
|
||||||
/// <see cref="ClientObject.Effects"/>.</summary>
|
/// <see cref="ClientObject.Effects"/>.</summary>
|
||||||
public const uint UiEffectsPropertyId = 18u;
|
public const uint UiEffectsPropertyId = 18u;
|
||||||
public const uint CurrentWieldedLocationPropertyId = 10u;
|
public const uint CurrentWieldedLocationPropertyId = 10u;
|
||||||
|
public const uint HookTypePropertyId = 151u;
|
||||||
|
public const uint HookItemTypesPropertyId = 152u;
|
||||||
public const uint SharedCooldownPropertyId = 280u;
|
public const uint SharedCooldownPropertyId = 280u;
|
||||||
public const uint CooldownDurationPropertyId = 167u;
|
public const uint CooldownDurationPropertyId = 167u;
|
||||||
|
|
||||||
|
|
@ -663,6 +665,8 @@ public sealed class ClientObjectTable
|
||||||
if (propertyId == SharedCooldownPropertyId) item.CooldownId = (uint)value;
|
if (propertyId == SharedCooldownPropertyId) item.CooldownId = (uint)value;
|
||||||
if (propertyId == CurrentWieldedLocationPropertyId)
|
if (propertyId == CurrentWieldedLocationPropertyId)
|
||||||
item.CurrentlyEquippedLocation = (EquipMask)(uint)value;
|
item.CurrentlyEquippedLocation = (EquipMask)(uint)value;
|
||||||
|
if (propertyId == HookTypePropertyId) item.HookType = (uint)value;
|
||||||
|
if (propertyId == HookItemTypesPropertyId) item.HookItemTypes = (uint)value;
|
||||||
if (propertyId == CurrentWieldedLocationPropertyId)
|
if (propertyId == CurrentWieldedLocationPropertyId)
|
||||||
UpdateEquipmentIndex(itemId, previous, ClientObjectPlacement.From(item));
|
UpdateEquipmentIndex(itemId, previous, ClientObjectPlacement.From(item));
|
||||||
ObjectUpdated?.Invoke(item);
|
ObjectUpdated?.Invoke(item);
|
||||||
|
|
@ -764,6 +768,8 @@ public sealed class ClientObjectTable
|
||||||
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
||||||
if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic;
|
if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic;
|
||||||
if (d.ContainersCapacity is { } cc) obj.ContainersCapacity = cc;
|
if (d.ContainersCapacity is { } cc) obj.ContainersCapacity = cc;
|
||||||
|
if (d.HookItemTypes is { } hookItemTypes) obj.HookItemTypes = hookItemTypes;
|
||||||
|
if (d.HookType is { } hookType) obj.HookType = hookType;
|
||||||
if (d.Structure is { } st) obj.Structure = st;
|
if (d.Structure is { } st) obj.Structure = st;
|
||||||
if (d.MaxStructure is { } ms) obj.MaxStructure = ms;
|
if (d.MaxStructure is { } ms) obj.MaxStructure = ms;
|
||||||
if (d.Workmanship is { } wm) obj.Workmanship = wm;
|
if (d.Workmanship is { } wm) obj.Workmanship = wm;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
|
|
@ -47,6 +48,8 @@ public sealed class AppraisalUiControllerTests
|
||||||
|
|
||||||
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
||||||
var properties = new PropertyBundle();
|
var properties = new PropertyBundle();
|
||||||
|
properties.Ints[19u] = 1_250;
|
||||||
|
properties.Ints[5u] = 350;
|
||||||
properties.Strings[16u] = "A finely balanced weapon.";
|
properties.Strings[16u] = "A finely balanced weapon.";
|
||||||
properties.Strings[7u] = "Remember the fallen.";
|
properties.Strings[7u] = "Remember the fallen.";
|
||||||
properties.Strings[8u] = "Tester";
|
properties.Strings[8u] = "Tester";
|
||||||
|
|
@ -62,6 +65,13 @@ public sealed class AppraisalUiControllerTests
|
||||||
UiText itemText = Assert.IsType<UiText>(
|
UiText itemText = Assert.IsType<UiText>(
|
||||||
layout.FindElement(AppraisalUiController.ItemTextId));
|
layout.FindElement(AppraisalUiController.ItemTextId));
|
||||||
Assert.Equal(VJustify.Top, itemText.VerticalJustify);
|
Assert.Equal(VJustify.Top, itemText.VerticalJustify);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
new Vector4(1f, 1f, 1f, 1f),
|
||||||
|
new Vector4(0f, 1f, 0f, 1f),
|
||||||
|
new Vector4(1f, 0f, 0f, 1f),
|
||||||
|
],
|
||||||
|
itemText.FontColorPalette);
|
||||||
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
||||||
Assert.Contains("Value: 1,250", report);
|
Assert.Contains("Value: 1,250", report);
|
||||||
Assert.Contains("Burden: 350", report);
|
Assert.Contains("Burden: 350", report);
|
||||||
|
|
@ -597,6 +607,8 @@ public sealed class AppraisalUiControllerTests
|
||||||
ObjectId = ObjectId,
|
ObjectId = ObjectId,
|
||||||
Name = "Hooked Decoration",
|
Name = "Hooked Decoration",
|
||||||
ContainerId = 0x50000002u,
|
ContainerId = 0x50000002u,
|
||||||
|
HookItemTypes = (uint)ItemType.Misc,
|
||||||
|
HookType = 1u,
|
||||||
});
|
});
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
using var interaction = NewInteraction(objects, []);
|
using var interaction = NewInteraction(objects, []);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
@ -22,6 +24,8 @@ public sealed class ItemAppraisalTextFormatterTests
|
||||||
};
|
};
|
||||||
var properties = new PropertyBundle();
|
var properties = new PropertyBundle();
|
||||||
properties.Ints[353u] = 2;
|
properties.Ints[353u] = 2;
|
||||||
|
properties.Ints[19u] = 12_500;
|
||||||
|
properties.Ints[5u] = 450;
|
||||||
properties.Ints[204u] = 4;
|
properties.Ints[204u] = 4;
|
||||||
properties.Ints[106u] = 300;
|
properties.Ints[106u] = 300;
|
||||||
properties.Ints[107u] = 250;
|
properties.Ints[107u] = 250;
|
||||||
|
|
@ -83,6 +87,232 @@ public sealed class ItemAppraisalTextFormatterTests
|
||||||
Assert.EndsWith("A finely balanced enchanted blade.", report);
|
Assert.EndsWith("A finely balanced enchanted blade.", report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HookWithHookProfile_UsesRetailUnknownsAndSuppressesBogusCapacity()
|
||||||
|
{
|
||||||
|
var obj = new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x50000005u,
|
||||||
|
Name = "Black Phyntos Hive",
|
||||||
|
Type = ItemType.Container,
|
||||||
|
ItemsCapacity = 255,
|
||||||
|
ContainersCapacity = 255,
|
||||||
|
HookItemTypes = (uint)ItemType.Container,
|
||||||
|
HookType = 4u,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
hook: new AppraiseInfoParser.HookProfile(
|
||||||
|
Flags: 0u,
|
||||||
|
ValidLocations: 0u,
|
||||||
|
AmmoType: 0u)),
|
||||||
|
_ => 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 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]
|
[Fact]
|
||||||
public void ArmorRequirementsAndUses_UseRetailLabelsAndResistanceBands()
|
public void ArmorRequirementsAndUses_UseRetailLabelsAndResistanceBands()
|
||||||
{
|
{
|
||||||
|
|
@ -215,7 +445,9 @@ public sealed class ItemAppraisalTextFormatterTests
|
||||||
PropertyBundle properties,
|
PropertyBundle properties,
|
||||||
uint[]? spells = null,
|
uint[]? spells = null,
|
||||||
AppraiseInfoParser.WeaponProfile? weapon = null,
|
AppraiseInfoParser.WeaponProfile? weapon = null,
|
||||||
AppraiseInfoParser.ArmorProfile? armor = null)
|
AppraiseInfoParser.ArmorProfile? armor = null,
|
||||||
|
AppraiseInfoParser.HookProfile? hook = null,
|
||||||
|
(ushort Highlight, ushort Color)? weaponEnchantments = null)
|
||||||
=> new(
|
=> new(
|
||||||
Guid: 0x50000001u,
|
Guid: 0x50000001u,
|
||||||
Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable,
|
Flags: AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable,
|
||||||
|
|
@ -225,10 +457,10 @@ public sealed class ItemAppraisalTextFormatterTests
|
||||||
ArmorProfile: armor,
|
ArmorProfile: armor,
|
||||||
CreatureProfile: null,
|
CreatureProfile: null,
|
||||||
WeaponProfile: weapon,
|
WeaponProfile: weapon,
|
||||||
HookProfile: null,
|
HookProfile: hook,
|
||||||
ArmorLevels: null,
|
ArmorLevels: null,
|
||||||
ArmorEnchantments: null,
|
ArmorEnchantments: null,
|
||||||
WeaponEnchantments: null,
|
WeaponEnchantments: weaponEnchantments,
|
||||||
ResistEnchantments: null);
|
ResistEnchantments: null);
|
||||||
|
|
||||||
private static SpellMetadata Spell(
|
private static SpellMetadata Spell(
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,43 @@ public sealed class CreateObjectTests
|
||||||
Assert.Equal(7.5f, p.Workmanship);
|
Assert.Equal(7.5f, p.Workmanship);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_HookIdentityFields_AreCapturedWithoutMisaligningOverlay()
|
||||||
|
{
|
||||||
|
const uint hookItemTypes = (uint)ItemType.Container;
|
||||||
|
const ushort hookType = 4;
|
||||||
|
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||||
|
guid: 0x50000027u,
|
||||||
|
name: "Black Phyntos Hive",
|
||||||
|
itemType: (uint)ItemType.Container,
|
||||||
|
weenieFlags: 0x20000000u | 0x10000000u | 0x40000000u,
|
||||||
|
hookItemTypes: hookItemTypes,
|
||||||
|
hookType: hookType,
|
||||||
|
iconOverlayId: 0x4567u);
|
||||||
|
|
||||||
|
var parsed = CreateObject.TryParse(body);
|
||||||
|
|
||||||
|
Assert.NotNull(parsed);
|
||||||
|
Assert.Equal(hookItemTypes, parsed.Value.HookItemTypes);
|
||||||
|
Assert.Equal((uint)hookType, parsed.Value.HookType);
|
||||||
|
Assert.Equal(0x06004567u, parsed.Value.IconOverlayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_AbsentHookIdentityFields_RemainNull()
|
||||||
|
{
|
||||||
|
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||||
|
guid: 0x50000028u,
|
||||||
|
name: "Ordinary Chest",
|
||||||
|
itemType: (uint)ItemType.Container);
|
||||||
|
|
||||||
|
var parsed = CreateObject.TryParse(body);
|
||||||
|
|
||||||
|
Assert.NotNull(parsed);
|
||||||
|
Assert.Null(parsed.Value.HookItemTypes);
|
||||||
|
Assert.Null(parsed.Value.HookType);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryParse_MovementSequence_SurfacedFromTimestampBlock()
|
public void TryParse_MovementSequence_SurfacedFromTimestampBlock()
|
||||||
{
|
{
|
||||||
|
|
@ -709,7 +746,9 @@ public sealed class CreateObjectTests
|
||||||
uint cooldownId = 0,
|
uint cooldownId = 0,
|
||||||
double cooldownDuration = 0,
|
double cooldownDuration = 0,
|
||||||
uint petOwnerId = 0,
|
uint petOwnerId = 0,
|
||||||
ushort spellId = 0)
|
ushort spellId = 0,
|
||||||
|
uint hookItemTypes = 0,
|
||||||
|
ushort hookType = 0)
|
||||||
{
|
{
|
||||||
var bytes = new List<byte>();
|
var bytes = new List<byte>();
|
||||||
WriteU32(bytes, CreateObject.Opcode);
|
WriteU32(bytes, CreateObject.Opcode);
|
||||||
|
|
@ -809,9 +848,9 @@ public sealed class CreateObjectTests
|
||||||
WriteU16(bytes, 0); // count
|
WriteU16(bytes, 0); // count
|
||||||
WriteU16(bytes, 768); // numBuckets (retail constant)
|
WriteU16(bytes, 768); // numBuckets (retail constant)
|
||||||
}
|
}
|
||||||
if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, 0); // HookItemTypes u32
|
if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, hookItemTypes); // HookItemTypes u32
|
||||||
if ((weenieFlags & 0x00000040u) != 0) WriteU32(bytes, 0); // Monarch u32
|
if ((weenieFlags & 0x00000040u) != 0) WriteU32(bytes, 0); // Monarch u32
|
||||||
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16
|
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, hookType); // HookType u16
|
||||||
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
|
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
|
||||||
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
|
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
|
||||||
if ((weenieFlags & 0x80000000u) != 0) WriteU32(bytes, materialType); // MaterialType
|
if ((weenieFlags & 0x80000000u) != 0) WriteU32(bytes, materialType); // MaterialType
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ public sealed class ObjectTableWiringTests
|
||||||
Priority = 0x00000005u,
|
Priority = 0x00000005u,
|
||||||
ItemsCapacity = 0,
|
ItemsCapacity = 0,
|
||||||
ContainersCapacity = 0,
|
ContainersCapacity = 0,
|
||||||
|
HookItemTypes = (uint)ItemType.Container,
|
||||||
|
HookType = 4u,
|
||||||
Structure = 80,
|
Structure = 80,
|
||||||
MaxStructure = 100,
|
MaxStructure = 100,
|
||||||
Workmanship = 4.5f,
|
Workmanship = 4.5f,
|
||||||
|
|
@ -102,6 +104,8 @@ public sealed class ObjectTableWiringTests
|
||||||
// --- capacity ---
|
// --- capacity ---
|
||||||
Assert.Equal(0, d.ItemsCapacity);
|
Assert.Equal(0, d.ItemsCapacity);
|
||||||
Assert.Equal(0, d.ContainersCapacity);
|
Assert.Equal(0, d.ContainersCapacity);
|
||||||
|
Assert.Equal((uint)ItemType.Container, d.HookItemTypes);
|
||||||
|
Assert.Equal(4u, d.HookType);
|
||||||
|
|
||||||
// --- durability ---
|
// --- durability ---
|
||||||
Assert.Equal(80, d.Structure);
|
Assert.Equal(80, d.Structure);
|
||||||
|
|
|
||||||
|
|
@ -300,12 +300,16 @@ public sealed class ClientObjectTableTests
|
||||||
var o = new ClientObject
|
var o = new ClientObject
|
||||||
{
|
{
|
||||||
ObjectId = 1, WielderId = 0x42u, ItemsCapacity = 24, ContainersCapacity = 7,
|
ObjectId = 1, WielderId = 0x42u, ItemsCapacity = 24, ContainersCapacity = 7,
|
||||||
|
HookItemTypes = (uint)ItemType.Container, HookType = 4u,
|
||||||
ContainerTypeHint = 1u, Priority = 8u, Structure = 5, MaxStructure = 10, Workmanship = 7.5f,
|
ContainerTypeHint = 1u, Priority = 8u, Structure = 5, MaxStructure = 10, Workmanship = 7.5f,
|
||||||
};
|
};
|
||||||
o.WeenieClassId = 0xABCDu; // now settable
|
o.WeenieClassId = 0xABCDu; // now settable
|
||||||
Assert.Equal(0x42u, o.WielderId);
|
Assert.Equal(0x42u, o.WielderId);
|
||||||
Assert.Equal(24, o.ItemsCapacity);
|
Assert.Equal(24, o.ItemsCapacity);
|
||||||
Assert.Equal(7, o.ContainersCapacity);
|
Assert.Equal(7, o.ContainersCapacity);
|
||||||
|
Assert.Equal((uint)ItemType.Container, o.HookItemTypes);
|
||||||
|
Assert.Equal(4u, o.HookType);
|
||||||
|
Assert.True(o.IsHook);
|
||||||
Assert.Equal(1u, o.ContainerTypeHint);
|
Assert.Equal(1u, o.ContainerTypeHint);
|
||||||
Assert.Equal(8u, o.Priority);
|
Assert.Equal(8u, o.Priority);
|
||||||
Assert.Equal(5, o.Structure);
|
Assert.Equal(5, o.Structure);
|
||||||
|
|
@ -314,6 +318,40 @@ public sealed class ClientObjectTableTests
|
||||||
Assert.Equal(0xABCDu, o.WeenieClassId);
|
Assert.Equal(0xABCDu, o.WeenieClassId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IngestAndLiveProperties_PreserveRetailHookIdentity()
|
||||||
|
{
|
||||||
|
const uint guid = 0x500000AEu;
|
||||||
|
var table = new ClientObjectTable();
|
||||||
|
ClientObject obj = table.Ingest(
|
||||||
|
FullWeenie(guid) with
|
||||||
|
{
|
||||||
|
HookItemTypes = (uint)ItemType.Container,
|
||||||
|
HookType = 4u,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(obj.IsHook);
|
||||||
|
Assert.Equal((uint)ItemType.Container, obj.HookItemTypes);
|
||||||
|
Assert.Equal(4u, obj.HookType);
|
||||||
|
|
||||||
|
Assert.True(table.UpdateIntProperty(
|
||||||
|
guid,
|
||||||
|
ClientObjectTable.HookTypePropertyId,
|
||||||
|
value: 0));
|
||||||
|
Assert.False(obj.IsHook);
|
||||||
|
Assert.True(table.UpdateIntProperty(
|
||||||
|
guid,
|
||||||
|
ClientObjectTable.HookTypePropertyId,
|
||||||
|
value: 2));
|
||||||
|
Assert.True(table.UpdateIntProperty(
|
||||||
|
guid,
|
||||||
|
ClientObjectTable.HookItemTypesPropertyId,
|
||||||
|
value: (int)ItemType.Misc));
|
||||||
|
Assert.True(obj.IsHook);
|
||||||
|
Assert.Equal(2u, obj.HookType);
|
||||||
|
Assert.Equal((uint)ItemType.Misc, obj.HookItemTypes);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WeenieData_Construct()
|
public void WeenieData_Construct()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue