Carry PublicWeenieDesc material type into the live object model so examination titles use the DAT-authored material prefix. Preserve retail AddItemInfo empty appends and embedded armor separator, restoring the deliberate blank rows between appraisal sections. Co-authored-by: Codex <codex@openai.com>
857 lines
33 KiB
Markdown
857 lines
33 KiB
Markdown
# Retail appraisal request and examination UI pseudocode
|
|
|
|
**Date:** 2026-07-23
|
|
**Scope:** status-bar Assess request, IdentifyObjectResponse parsing, and the
|
|
retained examination window.
|
|
|
|
## Sources
|
|
|
|
- Named retail:
|
|
- `gmToolbarUI::ListenToElementMessage @ 0x004BEE90`
|
|
- `gmExaminationUI::RecvNotice_ExamineObject @ 0x004AB7B0`
|
|
- `gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0`
|
|
- `gmExaminationUI::SetActiveExamineUI @ 0x004AB420`
|
|
- `gmExaminationUI::UseTime @ 0x004AB530`
|
|
- `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`
|
|
- `BasicCreatureExamineUI::Init @ 0x004AB9C0`
|
|
- `BasicCreatureExamineUI::BasicCreatureExamineUI @ 0x004ACD30`
|
|
- `BasicCreatureExamineUI::SetLevelValueText @ 0x004B3E70`
|
|
- `BasicCreatureExamineUI::SetAppraiseInfo @ 0x004B3F70`
|
|
- `CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0`
|
|
- `CharExamineUI::SetAppraiseInfo @ 0x004B45F0`
|
|
- `AttributeInfoRegion::Update @ 0x004F1D90`
|
|
- `Attribute2ndInfoRegion::Update @ 0x004F1E80`
|
|
- `AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0`
|
|
- `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::HandleInscriptionMousePresses @ 0x004AC230`
|
|
- `ItemExamineUI::SetInscriptionEditableState @ 0x004AC720`
|
|
- `ItemExamineUI::SetInscription @ 0x004AE2F0`
|
|
- `ItemExamineUI::HandleInscriptionLosingFocus @ 0x004AE620`
|
|
- `gmFloatyExaminationUI::ResizeTo @ 0x004D4470`
|
|
- `AppraisalProfile::IsHookedItemInscribable @ 0x005B2F90`
|
|
- `HookAppraisalProfile::IsInscribable @ 0x005B64C0`
|
|
- `CM_Writing::Event_SetInscription @ 0x006A98B0`
|
|
- `PublicWeenieDesc::BF_INSCRIBABLE` in `acclient.h:6434`
|
|
- Retail LayoutDesc `0x2100006B`, root `0x100005F2`
|
|
(`gmFloatyExaminationUI`, authored size 310 x 400 pixels).
|
|
- ACE:
|
|
- `IdentifyResponseFlags`
|
|
- `GameEventIdentifyObjectResponse`
|
|
- `AppraiseInfo.Write`
|
|
- `HookProfile.Write`
|
|
- `GameActionSetInscription`
|
|
- `Player.HandleActionSetInscription`
|
|
- ACViewer's `HookAppraisalProfile` representation, used as a second
|
|
interpretation check for the three-field hook profile.
|
|
|
|
The named retail client is the behavior oracle. ACE and ACViewer only clarify
|
|
the server packet and structure interpretation.
|
|
|
|
## Toolbar request
|
|
|
|
```text
|
|
on Examine button:
|
|
if selected object id != 0:
|
|
ClientUISystem.ExamineObject(selected id)
|
|
else:
|
|
enter TARGET_MODE_EXAMINE
|
|
```
|
|
|
|
This was already present in acdream before this slice and does not need a
|
|
second command path.
|
|
|
|
## Examination request lifetime
|
|
|
|
```text
|
|
on ExamineObject(guid):
|
|
if guid == 0:
|
|
return
|
|
|
|
if awaitingAppraisalId == 0:
|
|
ClientUISystem.IncrementBusyCount()
|
|
|
|
awaitingAppraisalId = guid
|
|
send CM_Item.Event_Appraise(guid)
|
|
clear pending profile pointer
|
|
```
|
|
|
|
Replacing one pending GUID does not add a second busy reference. The response
|
|
for the current pending GUID releases that one reference.
|
|
|
|
## IdentifyObjectResponse packet order
|
|
|
|
The flag values are part of the wire contract and are not the field-order
|
|
indices:
|
|
|
|
```text
|
|
IntStatsTable 0x0001
|
|
BoolStatsTable 0x0002
|
|
FloatStatsTable 0x0004
|
|
StringStatsTable 0x0008
|
|
SpellBook 0x0010
|
|
WeaponProfile 0x0020
|
|
HookProfile 0x0040
|
|
ArmorProfile 0x0080
|
|
CreatureProfile 0x0100
|
|
ArmorEnchantmentBitfield 0x0200
|
|
ResistEnchantmentBitfield 0x0400
|
|
WeaponEnchantmentBitfield 0x0800
|
|
DidStatsTable 0x1000
|
|
Int64StatsTable 0x2000
|
|
ArmorLevels 0x4000
|
|
```
|
|
|
|
ACE writes the gated bodies in the order below, independent of the numerical
|
|
position of each flag.
|
|
|
|
```text
|
|
read guid
|
|
read flags
|
|
read success
|
|
|
|
if IntStatsTable: read packed int table
|
|
if Int64StatsTable: read packed int64 table
|
|
if BoolStatsTable: read packed bool table
|
|
if FloatStatsTable: read packed double table
|
|
if StringStatsTable: read packed string table
|
|
if DidStatsTable: read packed DID table
|
|
if SpellBook: read spell list
|
|
if ArmorProfile: read armor profile
|
|
if CreatureProfile: read creature profile
|
|
if WeaponProfile: read weapon profile
|
|
if HookProfile: read flags, validLocations, ammoType (three u32)
|
|
if ArmorEnchantmentBitfield: read highlight/color (two u16)
|
|
if WeaponEnchantmentBitfield: read highlight/color (two u16)
|
|
if ResistEnchantmentBitfield: read highlight/color (two u16)
|
|
if ArmorLevels: read nine u32 armor levels
|
|
```
|
|
|
|
Every gated field is positional. A truncated gated field rejects the packet;
|
|
returning a partial profile would let later fields masquerade as earlier ones.
|
|
|
|
## Applying an appraisal
|
|
|
|
```text
|
|
SetAppraiseInfo(guid, profile):
|
|
if guid == 0:
|
|
return
|
|
if guid != awaitingAppraisalId and guid != currentAppraisalId:
|
|
return
|
|
|
|
firstResponse = false
|
|
if guid == awaitingAppraisalId:
|
|
awaitingAppraisalId = 0
|
|
ClientUISystem.DecrementBusyCount()
|
|
currentAppraisalId = guid
|
|
firstResponse = true
|
|
|
|
if profile has no CreatureProfile:
|
|
subview = item
|
|
else if profile has string property Template (5)
|
|
or int property EquipmentSetId (0x105):
|
|
subview = character/player
|
|
else:
|
|
subview = creature
|
|
|
|
object = ClientObjMaintSystem.GetWeenieObject(guid)
|
|
if object is absent:
|
|
return
|
|
|
|
titleName = profile string 0x34 if present
|
|
else object.GetObjectName(NAME_APPROPRIATE)
|
|
title = titleName with stack count
|
|
|
|
newlySelectedInSubview = guid != subview.currentObjectId
|
|
subview.Init(guid, object)
|
|
subview.SetAppraiseInfo(profile, newlySelectedInSubview)
|
|
SetActiveExamineUI(subview)
|
|
|
|
if firstResponse:
|
|
show examination window
|
|
```
|
|
|
|
`ACCWeenieObject::GetObjectName @ 0x0058E6E0` does more than pluralize:
|
|
|
|
```text
|
|
baseName = singular or plural name selected for the live stack size
|
|
if PublicWeenieDesc.MaterialType > 0:
|
|
material = AppraisalSystem.InqMaterialName(MaterialType)
|
|
remove material from baseName when it is already authored there
|
|
trim the remaining baseName
|
|
baseName = material + " " + remaining baseName
|
|
return baseName
|
|
```
|
|
|
|
The title therefore uses the live CreateObject material field. For example,
|
|
an authored `Steel Toed Boots` object whose material is `Reed Shark Hide`
|
|
is titled `Reed Shark Hide Steel Toed Boots`; appraisal property 131 is for
|
|
description decoration and is not a substitute for this live name field.
|
|
|
|
Only the first response to an explicit request forces the window visible.
|
|
Background refreshes update the current object without reopening a closed
|
|
window.
|
|
|
|
## Subview and refresh behavior
|
|
|
|
```text
|
|
SetActiveExamineUI(selected):
|
|
hide item, creature, character, and spell subviews
|
|
show selected subview only
|
|
|
|
on time pulse:
|
|
if examination window is visible
|
|
and combat mode is not peace
|
|
and active subview is creature or character
|
|
and currentAppraisalId != 0
|
|
and 0.75 seconds elapsed:
|
|
send another Event_Appraise(currentAppraisalId)
|
|
```
|
|
|
|
`gmExaminationUI::gmExaminationUI @ 0x004AB2B0` initializes
|
|
`m_examineNewlySelectedItem` to one. Selection-follow is therefore retail's
|
|
default examination behavior, not an optional preference:
|
|
|
|
```text
|
|
on SelectionChanged:
|
|
if examination window is not visible:
|
|
return
|
|
if selected object id != 0:
|
|
ClientUISystem.ExamineObject(selected id)
|
|
else:
|
|
hide examination window
|
|
```
|
|
|
|
## Creature presentation
|
|
|
|
`BasicCreatureExamineUI` binds viewport `0x10000148`, base-stat list box
|
|
`0x10000149`, miscellaneous-rating list box `0x10000335`, level value
|
|
`0x1000014C`, and creature display name `0x1000014E`. Its rows are instances
|
|
of LayoutDesc `0x2100006B` template `0x10000166`, whose label and value fields
|
|
are `0x1000012A` and `0x1000012B`.
|
|
|
|
```text
|
|
on creature appraisal:
|
|
level = int property 25
|
|
level text = decimal level when positive, otherwise "???"
|
|
|
|
creatureType = int property 2
|
|
display name = EnumMapper 0x2200000E[creatureType]
|
|
replace underscores in display name with spaces
|
|
|
|
clear stat list
|
|
append rows in this exact order:
|
|
Strength primary attribute 1
|
|
Endurance primary attribute 2
|
|
Coordination primary attribute 4
|
|
Quickness primary attribute 3
|
|
Focus primary attribute 5
|
|
Self primary attribute 6
|
|
Health current secondary 2 / maximum secondary 1, show percent
|
|
Stamina current secondary 4 / maximum secondary 3
|
|
Mana current secondary 6 / maximum secondary 5
|
|
|
|
clear miscellaneous list
|
|
damageRating = int property 0x133
|
|
damageResistRating = int property 0x134
|
|
critRating = int property 0x139
|
|
critDamageRating = int property 0x13A
|
|
critResistRating = int property 0x13B
|
|
critDamageResist = int property 0x13C
|
|
healingBoostRating = int property 0x143
|
|
dotResistRating = int property 0x15E
|
|
lifeResistRating = int property 0x15F
|
|
|
|
if any displayed rating group is nonzero:
|
|
append blank row
|
|
if damageRating or critRating or critDamageRating is positive:
|
|
append "Dmg/CritDmg" = "Rating: damageRating/critDamageRating"
|
|
if damageResistRating or critResistRating or critDamageResist is positive:
|
|
append "Dmg/CritDmg" = "Resist: damageResistRating/critDamageResist"
|
|
if dotResistRating or lifeResistRating is positive:
|
|
append "DoT/Life:" = "Resist: dotResistRating/lifeResistRating"
|
|
append blank row
|
|
```
|
|
|
|
Successful primary values are decimal integers. Successful Health is
|
|
`current/max (percent %)`; Stamina and Mana are `current/max`. On a failed
|
|
assessment, Health still shows the percentage when both values are known,
|
|
while every other row is `???`. The profile highlight/color bitfields select
|
|
one of retail's four authored FontInfo entries (normal, beneficial,
|
|
detrimental, or incomplete); exact FontInfo-list binding remains AP-110.
|
|
The 2013 client reads HealingBoost but does not append it to this creature
|
|
rating list. CritRating and CritResistRating determine whether their paired
|
|
row exists, while the displayed right-hand value is CritDamageRating or
|
|
CritDamageResistRating exactly as retail formats it.
|
|
|
|
The authored viewport and list overlap. Retail draws the creature between the
|
|
row chrome and the row text. The modern retained RTT seam reproduces that
|
|
ordering with two synchronized copies of the same DAT row template: media-only
|
|
chrome below the viewport and media-free label/value descendants above it.
|
|
Both copies share one pixel scroll model. The 292-pixel authored row is inset
|
|
four pixels within the 300-pixel list surface, giving the text a balanced
|
|
left/right margin without changing template geometry.
|
|
|
|
The animated preview is a private `CreatureMode`, not the live world object:
|
|
|
|
```text
|
|
clear the previous private creature
|
|
clone selected CPhysicsObj with makeObject
|
|
set clone heading to 191.367905 degrees
|
|
query the clone bounding box
|
|
|
|
width = bbox.max.x - bbox.min.x
|
|
depth = bbox.max.y - bbox.min.y
|
|
height = bbox.max.z - bbox.min.z
|
|
viewportHeightOverWidth = viewport.height / viewport.width
|
|
fittedVerticalSpan = max(height, width * viewportHeightOverWidth)
|
|
center = (bbox.min + bbox.max) / 2
|
|
camera.position = (
|
|
center.x,
|
|
center.y - (fittedVerticalSpan * 1.20710683 + depth / 2),
|
|
center.z)
|
|
camera.direction = identity (+Y)
|
|
|
|
viewport light:
|
|
type = DISTANT_LIGHT
|
|
intensity = 2
|
|
direction = (0.3, 1.9, 0.65)
|
|
|
|
insert clone into the private CreatureMode
|
|
```
|
|
|
|
ACDream's modern rendering equivalent uses the same isolated render-to-texture
|
|
owner as the inventory paperdoll. The examination clone has its own stable
|
|
render identity, fixed heading and retail camera/light, while its mesh-part
|
|
transforms are refreshed from the live target each frame so its current
|
|
animation is visible without registering a second gameplay entity. Retail's
|
|
clone instead advances the motion state copied by
|
|
`CPhysicsObj::MorphToExistingObject`; that narrow sequencer-ownership
|
|
adaptation is recorded under AP-92.
|
|
|
|
## Item presentation order
|
|
|
|
`ItemExamineUI::SetAppraiseInfo` clears and rebuilds the authored item text in
|
|
this order:
|
|
|
|
1. icon;
|
|
2. value and burden (the EoR layout has no separate value/burden controls, so
|
|
these enter the main text);
|
|
3. inscription/signature;
|
|
4. tinkering/workmanship;
|
|
5. set and rating information;
|
|
6. weapon or armor profile;
|
|
7. defense and armor modifiers;
|
|
8. magic, special properties, usage and wield requirements;
|
|
9. caster/boost/healer/capacity/lock/mana/uses/craftsman details;
|
|
10. sale restriction, rare status, full magic, and description.
|
|
|
|
The individual branches are not a generic property dump. They project the
|
|
wire profile into retail prose:
|
|
|
|
```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
|
|
|
|
# Empty text is meaningful. The separator is still appended, so
|
|
# AddItemInfo("", ..., true) preserves an intentional blank row.
|
|
|
|
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
|
|
|
|
UnpackCapacityByte(wireByte):
|
|
# PublicWeenieDesc::UnPack uses MOVSX at 0x005AD530 and 0x005AD545.
|
|
# The generated pseudocode's uint8_t temporary is misleading.
|
|
return sign_extend_int8_to_int32(wireByte)
|
|
|
|
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, so the hook's own
|
|
# capacity is not player-facing capacity.
|
|
|
|
# Ordinary creature/NPC-lookalike templates can carry -1/-1 capacity
|
|
# sentinels. They travel as FF/FF but UnpackCapacityByte restores -1/-1,
|
|
# so the routine's > 0 tests emit no capacity prose. Black Phyntos Hive
|
|
# (WCID 28249) is this case: Creature, not Hook.
|
|
|
|
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):
|
|
ammoType = live PublicWeenieDesc ammo type
|
|
if object is a hook:
|
|
ammoType = HookProfile ammo type
|
|
|
|
if validLocations contains Shield:
|
|
append "Base Shield Level: <ArmorLevel>"
|
|
append the player-dependent effective Shield level when available
|
|
|
|
if validLocations contains a weapon location:
|
|
weapon = profile.WeaponProfile
|
|
append "Skill: <WeaponSkill>"
|
|
if WeaponType exists:
|
|
append the parenthesized weapon subtype
|
|
|
|
launcher = validLocations contains MissileWeapon and ammoType != None
|
|
append launcher ? "Damage Bonus: " : "Damage: "
|
|
minimum = (1 - DamageVariance) * Damage
|
|
if Damage - minimum > 0.0002:
|
|
append "<minimum> - <maximum>"
|
|
else:
|
|
append "<maximum>"
|
|
append damage type for non-launchers
|
|
|
|
append Elemental Damage Bonus when present
|
|
append launcher Damage Modifier when present
|
|
|
|
if validLocations contains MeleeWeapon, MissileWeapon, or TwoHanded:
|
|
append "Speed: <category> (<raw weapon time>)"
|
|
categories:
|
|
raw < 11 -> Very Fast
|
|
raw < 31 -> Fast
|
|
raw < 50 -> Average
|
|
raw < 80 -> Slow
|
|
otherwise -> Very Slow
|
|
|
|
if launcher:
|
|
rangeYards = min(
|
|
85,
|
|
2 * MaxVelocity^2 * (1 / 9.8) * 1.094)
|
|
if rangeYards < 10:
|
|
displayedRange = ceil(rangeYards)
|
|
else:
|
|
displayedRange = truncate down to a multiple of 5
|
|
append "Range: <displayedRange> yds."
|
|
if MaxVelocityEstimated:
|
|
append " (based on STRENGTH 100)"
|
|
|
|
if WeaponOffense != 1 and this is not launcher damage:
|
|
append "Bonus to Attack Skill: <signed percent>."
|
|
|
|
append the retail ammunition sentence:
|
|
ammunition -> "Used as ammunition by bows/crossbows/atlatls."
|
|
launcher -> "Uses arrows/quarrels/atlatl darts as ammunition."
|
|
|
|
ShowArmor(profile):
|
|
append "Armor Level: <base AL>"
|
|
for Slashing, Piercing, Bludgeoning, Fire, Cold, Acid, Electric, Nether:
|
|
adjective from DamageResistanceToString:
|
|
0.0 -> None
|
|
(0.0, 0.4) -> Poor
|
|
[0.4, 0.8) -> Below Average
|
|
[0.8, 1.2) -> Average
|
|
[1.2, 1.6) -> Above Average
|
|
[1.6, 2.0) -> Excellent
|
|
>= 2.0 -> Unparalleled
|
|
append "<damage>: <adjective> (<base AL * modifier, rounded>)"
|
|
|
|
ShowMagic(profile):
|
|
if the spellbook exists and appraisal failed:
|
|
append "Spells: unknown."
|
|
return
|
|
|
|
split spell ids by the high enchantment bit
|
|
resolve every id with that bit masked through ClientMagicSystem
|
|
|
|
for ordinary spells:
|
|
append Spellcraft, Mana, and Mana Cost when present
|
|
append "Spell Descriptions:"
|
|
append each spell name and its DAT description
|
|
|
|
for high-bit enchantments:
|
|
append "Enchantments:"
|
|
append each spell name and its DAT description
|
|
|
|
ShowSpecialProperties(profile):
|
|
append carry limit and authored cooldown duration when present
|
|
append cleave count
|
|
build one "Properties: " line from:
|
|
creature slayer, Multi-Strike, imbued-effect flags,
|
|
Magic Absorbing, Unenchantable, Attuned/Bonded/death behavior,
|
|
Retained, Crushing Blow, Biting Strike, Armor/Resistance Cleaving,
|
|
Cast on Strike, Ivoryable, and Dyeable
|
|
if an imbued-effect flag exists:
|
|
append "This item cannot be further imbued."
|
|
if AutowieldLeft:
|
|
append the left-hand tether sentence
|
|
|
|
ShowRemainingItemFields(profile):
|
|
append, in SetAppraiseInfo order:
|
|
usage text
|
|
level, direct heritage restriction, and wield requirements
|
|
(up to four requirement triplets)
|
|
use restrictions
|
|
item level and item XP
|
|
activation requirements
|
|
caster, boost, healing, capacity, lock, and mana-stone data
|
|
remaining key/charge counts
|
|
creator, last tinkerer, and imbuer
|
|
cannot-be-sold and rare notices
|
|
short and long descriptions
|
|
```
|
|
|
|
### Exhaustive EoR item-report branches
|
|
|
|
The common projection above was re-audited against every helper called by
|
|
`ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`. The remaining item branches are
|
|
not generic property labels; retail applies the following exact rules.
|
|
|
|
```text
|
|
ShowTinkering:
|
|
if NumTimesTinkered (171):
|
|
"This item has been tinkered %d time[s]."
|
|
if TinkerName (39): "Last tinkered by %s."
|
|
if ImbuerName (40): "Imbued by %s."
|
|
if Workmanship (105):
|
|
if NumItemsInMaterial (170) is absent:
|
|
"Workmanship: <adjective> (<integer workmanship>)"
|
|
else:
|
|
average = workmanship / NumItemsInMaterial
|
|
"Workmanship: <adjective of rounded average> (<average, two decimals>)"
|
|
blank line
|
|
"Salvaged from %d items."
|
|
append an empty same-paragraph entry unconditionally
|
|
|
|
ShowSet:
|
|
EquipmentSetId (265) selects the literal EoR set-name table.
|
|
Unknown and explicitly unused entries produce no row.
|
|
Known entries produce "Set: <name>".
|
|
|
|
ShowRatings:
|
|
append only positive ratings, in this order:
|
|
Dam, Dam Resist, Crit, Crit Dam, Crit Resist, Crit Dam Resist,
|
|
Heal Boost, Nether Resist, Life Resist
|
|
render as "Ratings: Dam 3, Crit 2" (there is no plus sign)
|
|
positive Vitality (379) is a separate
|
|
"This item adds %d Vitality."
|
|
if any rating or Vitality was emitted:
|
|
append an empty same-paragraph entry
|
|
SetAppraiseInfo appends one more empty entry when Set or Ratings emitted
|
|
|
|
ShowWeaponAndArmorData:
|
|
only interpret WeaponProfile inside weapon/shield valid-location bits
|
|
preserve retail Unknown sentinels for damage, speed, and range
|
|
for ordinary clothing append:
|
|
"Covers Head, Chest, Abdomen, Upper Arms, Lower Arms, Hands,
|
|
Upper Legs, Lower Legs, Feet"
|
|
selecting the names from ClothingPriority bits in that exact order
|
|
|
|
ShowArmorModsData:
|
|
the first "Armor Level" string itself begins with "\n"
|
|
AddItemInfo also contributes its ordinary separator, leaving one blank
|
|
row between clothing coverage and Armor Level
|
|
|
|
ShowSpecialProperties:
|
|
begin with an empty same-paragraph entry
|
|
carry-limit text has another empty entry before it
|
|
shared cooldown metadata and cleave have their retail trailing empty entry
|
|
|
|
ShowShortMagic:
|
|
list only ordinary spell ids (high bit clear)
|
|
a failed appraisal prints "Spells: unknown."
|
|
|
|
ShowMagic:
|
|
a failed appraisal also prints "Spells: unknown."
|
|
ordinary spell rows are:
|
|
"Spell Descriptions:\n~ <name>: <DAT description>"
|
|
enchantment rows are:
|
|
"Enchantments:\n~ <name>: <DAT description>"
|
|
|
|
ShowLevelLimits:
|
|
min == max: "Restricted to characters of Level %d."
|
|
min + max: "Restricted to characters of Levels %d to %d."
|
|
min only: "Restricted to characters of Level %d or greater."
|
|
max only: "Restricted to characters of Level %d or below."
|
|
|
|
ShowItemLevel:
|
|
require ItemBaseXp (int64 5), ItemMaxLevel (319), and ItemXpStyle (320)
|
|
currentLevel = ItemTotalXPToLevel(ItemTotalXp, baseXp, maxLevel, style)
|
|
displayedLevel = min(currentLevel + 1, maxLevel)
|
|
nextThreshold = ItemLevelToTotalXP(min(currentLevel + 1, maxLevel), ...)
|
|
"Item Level: <displayedLevel> / <maxLevel>"
|
|
"Item XP: <grouped total> / <grouped nextThreshold>"
|
|
append an empty same-paragraph entry after Item XP
|
|
|
|
ItemLevelToTotalXP(level, base, max, style):
|
|
clamp level to [0, max]
|
|
style 1: base * level
|
|
style 2: base * (2^level - 1)
|
|
style 3: base * level * (level + 1) / 2
|
|
|
|
ShowActivationRequirements:
|
|
successful appraisals only
|
|
append Arcane Lore, Allegiance Rank, Heritage, skill, primary-attribute,
|
|
and secondary-attribute requirements to one "Activation requires " row
|
|
in that order
|
|
|
|
ShowBoostValue / ShowHealKitValues:
|
|
ordinary consumables render Health/Mana "... when used."
|
|
Stamina is "... when consumed."
|
|
healer and hooked-healer profiles instead interpret BoostValue as
|
|
"Bonus to Healing Skill" and float 100 as "Restoration Bonus"
|
|
|
|
ShowRareInfo:
|
|
RareUsesTimer (bool 108):
|
|
"This rare item has a timer restriction of 3 minutes. You will not
|
|
be able to use another rare item with a timer within 3 minutes of
|
|
using this one."
|
|
RareId (17): "Rare #%d"
|
|
|
|
ShowDescription:
|
|
if lifespan, creation timestamp, and remaining lifespan are present:
|
|
remaining < 0: "This item is in the act of disintegrating."
|
|
otherwise: "This item expires in <years/days/hours/minutes/seconds>"
|
|
LongDesc replaces ShortDesc when present; both are never printed
|
|
GearPlatingName (52) replaces LongDesc before decoration
|
|
AppraisalItemSkill (172) decorates that description:
|
|
bit 1 prepends workmanship
|
|
MaterialType (131) prepends the DAT material display name and removes
|
|
its first occurrence from the authored description
|
|
bit 4 appends ", set with <count> <pluralized material>"
|
|
PortalBitmask (111) appends, in bit order:
|
|
Player Killers may not use this portal.
|
|
Lite Player Killers may not use this portal.
|
|
Non-Player Killers may not use this portal.
|
|
This portal cannot be recalled nor linked to.
|
|
This portal cannot be summoned.
|
|
```
|
|
|
|
The material name is not a locally invented table.
|
|
`MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500` performs
|
|
`DBObj::GetByEnum(DBObjType=0x28, clientEnum=0x10000001, subEnum=1)`:
|
|
the master map resolves the root `EnumIDMap`, sub-enum 1 resolves the material
|
|
`DualEnumIDMap`, and its client name is read with underscores replaced by
|
|
spaces. Creature/slayer
|
|
names use `EnumMapper::GetString(0x10000005, value)`; heritage restrictions use
|
|
`EnumMapper::GetString(0x10000002, value)` with retail's explicit
|
|
Gharu'ndim/Umbraen/Olthoi spellings.
|
|
|
|
Additional named sources for these branches:
|
|
|
|
- `ItemExamineUI::Appraisal_ShowSet @ 0x004AE880`
|
|
- `ItemExamineUI::Appraisal_ShowRatings @ 0x004AF080`
|
|
- `ItemExamineUI::Appraisal_ShowTinkeringInfo @ 0x004B0E70`
|
|
- `ItemExamineUI::Appraisal_ShowLevelLimitInfo @ 0x004B2920`
|
|
- `ItemExamineUI::Appraisal_ShowItemLevelInfo @ 0x004B3C60`
|
|
- `ItemExamineUI::Appraisal_ShowDescription @ 0x004B6990`
|
|
- `ExperienceSystem::ItemLevelToTotalXP @ 0x005C8100`
|
|
- `ExperienceSystem::ItemTotalXPToLevel @ 0x005C81B0`
|
|
- `MaterialTypeEnumMapper::MaterialTypeToString @ 0x005CD500`
|
|
|
|
Sources:
|
|
|
|
- `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`
|
|
- `ItemExamineUI::Appraisal_ShowWieldRequirements @ 0x004AF9A0`
|
|
- `ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140`
|
|
- `ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0`
|
|
- `ItemExamineUI::Appraisal_ShowArmorModsData @ 0x004B1F60`
|
|
- `ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10`
|
|
- `ItemExamineUI::Appraisal_ShowActivationRequirements @ 0x004B3270`
|
|
- `AppraisalSystem::WeaponTimeToString @ 0x005B4970`
|
|
- `AppraisalSystem::DamageResistanceToString @ 0x005B5490`
|
|
- `AppraisalSystem::InqWorkmanshipAdjective @ 0x005B48E0`
|
|
- `ClientUISystem::DeltaTimeToString @ 0x00565E10`
|
|
|
|
ACE's `AppraiseInfo` serializer and property enums were used to pin the
|
|
typed-table IDs carried by the network profile. ACViewer's portal.dat
|
|
SpellTable path was used as the second reference check that item spell names
|
|
and descriptions come from DAT spell metadata rather than a hand-maintained
|
|
client list. Retail's named functions remain the behavioral oracle.
|
|
|
|
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
|
|
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
|
|
|
|
The LayoutDesc root already contains retail's complete floaty window chrome,
|
|
close control, item/creature/character/spell subviews, and scrollbars.
|
|
The root's concrete retail type is `gmFloatyExaminationUI`, not a child of
|
|
`gmPanelUI`. It is mounted and persisted as its own top-level retained window.
|
|
Showing it must not hide, replace, resize, or reposition the shared
|
|
Inventory/Skills/Spellbook panel.
|
|
`gmFloatyExaminationUI::ResizeTo` delegates to ordinary `UIElement::ResizeTo`
|
|
and persists width/height; it adds no special resize algorithm.
|
|
|
|
The authored root is exactly 310 by 400 pixels and has 310-by-400 minimum
|
|
constraints. Its maximum constraints are 2000 by 2000, so 310 by 400 is the
|
|
retail opening/default size, not a fixed-size panel. The main item report
|
|
control is authored with bottom vertical justification, but
|
|
`ItemExamineUI::AddItemInfo` appends rows from the start of the cleared text
|
|
surface. The retained projection must therefore lay out that generated report
|
|
from the top while leaving the separate authored inscription behavior intact.
|
|
|
|
## Inscription presentation and eligibility
|
|
|
|
```text
|
|
SetInscription(profile):
|
|
reset inscription field to its unfocused state
|
|
clear displayed text, signature, old inscription, scribe name/account
|
|
|
|
inscribable = object.PublicWeenieDesc has BF_INSCRIBABLE
|
|
if object is a hook:
|
|
inscribable = profile.HookProfile exists
|
|
and (HookProfile.Flags & 1) != 0
|
|
|
|
if not inscribable:
|
|
hide inscription field and signature
|
|
return
|
|
|
|
scribe = profile string property 8, or empty
|
|
oldInscription = profile string property 7, or empty
|
|
|
|
if scribe is empty:
|
|
field = "<Inscribe here>"
|
|
signature = ""
|
|
else:
|
|
field = oldInscription
|
|
signature = "--" + scribe
|
|
if local player is a PSR and scribe-account property 0x17 exists:
|
|
signature += " <" + scribeAccount + ">"
|
|
|
|
show field and signature
|
|
SetInscriptionEditableState()
|
|
|
|
SetInscriptionEditableState:
|
|
editable = object exists
|
|
and object has BF_INSCRIBABLE
|
|
and object is owned by the local player
|
|
and (scribe is empty
|
|
or case-insensitive scribe == local player name)
|
|
PSR may override the ownership/scribe restriction
|
|
field.Editable = editable
|
|
field.Selectable = editable
|
|
inscription background receives mouse presses only when not editable
|
|
```
|
|
|
|
The ordinary-player port does not fabricate PSR status. It applies the exact
|
|
ownership/name rule; an eventual PSR player-description capability can add the
|
|
retail override without changing this transaction.
|
|
|
|
For hooks, the appraisal HookProfile is the source of truth for presentation:
|
|
`AppraisalProfile::IsHookedItemInscribable` returns false when it is absent and
|
|
otherwise returns bit 0. The live public description remains the source used by
|
|
`SetInscriptionEditableState`, matching the two retail functions rather than
|
|
merging them into one guessed rule.
|
|
|
|
Noneditable mouse presses report retail's exact reason:
|
|
|
|
```text
|
|
if scribe is nonempty and differs from local player:
|
|
"Only <scribe> can change the inscription"
|
|
else if object is absent or not owned:
|
|
"Item must be in your inventory to inscribe."
|
|
else if BF_INSCRIBABLE is absent:
|
|
"This item is not inscribable."
|
|
```
|
|
|
|
## Inscription edit transaction
|
|
|
|
```text
|
|
on inscription focus gained:
|
|
if scribe is empty:
|
|
switch to the normal editing state
|
|
clear "<Inscribe here>"
|
|
signature = "--" + local player name
|
|
|
|
on inscription focus lost:
|
|
if current object id == 0:
|
|
return
|
|
|
|
text = current field text
|
|
skipSend = text is empty and original scribe is empty
|
|
if not skipSend and text != oldInscription:
|
|
send CM_Writing.Event_SetInscription(object id, text)
|
|
|
|
if text is empty:
|
|
restore unfocused state
|
|
field = "<Inscribe here>"
|
|
signature = ""
|
|
scribe name/account = ""
|
|
else:
|
|
scribe name = local player name
|
|
signature = "--" + local player name
|
|
|
|
oldInscription = text
|
|
```
|
|
|
|
`CM_Writing::Event_SetInscription` sends a normal UI-counter GameAction:
|
|
|
|
```text
|
|
u32 0xF7B1 GameAction envelope
|
|
u32 uiCounter
|
|
u32 0x00BF SetInscription
|
|
u32 objectGuid
|
|
String16L text CP-1252, padded to a four-byte boundary
|
|
```
|
|
|
|
There is no successful server reply in retail captures. The client therefore
|
|
updates its field/signature optimistically after sending; ACE deliberately
|
|
implements the same behavior.
|
|
|
|
## Remaining slice boundary
|
|
|
|
The request lifetime, exact packet flags, independent floaty window,
|
|
inscription permission/edit/write transaction, subview choice, authored outer
|
|
layout, ordered creature stats and ratings, animated creature preview,
|
|
viewport/list/text compositing, selection-follow, scrollbar ownership, combat
|
|
refresh, and the complete EoR item-report dispatch are in scope. Item rows now
|
|
preserve retail's appraisal-only unknown values, hook/capacity/lock presence
|
|
rules, exact paragraph boundaries, authored normal/beneficial/detrimental
|
|
font-color selection, equipment-set/ratings/tinkering/weapon/armor/magic/XP/
|
|
activation/healer/rare/description ordering, and installed-DAT material and
|
|
creature names. Retail's item-object preview, live player-dependent effective
|
|
shield/cooldown projections, localized augmentation-cost `StringInfo`,
|
|
creature incomplete/high/low FontInfo selection, complete character detail,
|
|
and PSR-only scribe-account/override behavior remain explicit AP-110 work.
|