Restore the authored examination geometry and top-origin item list, then port retail's ordered weapon, armor, magic, requirement, capacity, cooldown, use, and description branches into a dedicated formatter with DAT spell prose. Keep the remaining specialized display-name and preview gaps explicit in AP-110. Co-authored-by: Codex <codex@openai.com>
609 lines
22 KiB
Markdown
609 lines
22 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::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
|
|
```
|
|
|
|
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
|
|
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
|
|
```
|
|
|
|
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.
|
|
|
|
## 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, and
|
|
combat refresh are in scope. Retail's item-object preview, exhaustive
|
|
specialized `ItemExamineUI` detail branches, exact appraisal FontInfo-list
|
|
selection, and PSR-only scribe-account/override behavior remain explicit
|
|
AP-110 work.
|