Test on dev (character Moo): with Enhanced Strength held, the client shaded
an Increased Strength runestone red and showed its refusal popup, but the
server applied it as a downgrade. The client's 0x4768D0 (ArcRuneTemplate
vtable slot 0x1E8) blocks a talent when a held talent with the same
rune_sub_type has an equal or higher rank, rank 0 included. AddRune's
rank-0 replace branch is never reached past that check.
Value changes vs the previous commit:
- Rank-0 stat runes are refused while any rank of their chain is held
(same as the old DB type 4 block)
- Rank-0 talents in the same group (e.g. Bloodline sub-race runestones)
are refused instead of replacing each other
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RuneBase(RuneTemplate) fills the race, class and discipline maps from
item_race_req / item_class_req / item_disc_req (names to rune ids through
CObjectIndex; class runes with rune_is_standard_character_creation go to
baseClass, the rest to promotionClass) with a restrict flag for each, and
the level from item_level_req. static_rune_runereq is no longer used for
cached rune bases; GET_RUNE_REQS stays until 2.11.
ApplyRuneMsg follows ArcCharacter_MeetsItemReqs (0x475F40) and
ArcItemRestrict_Check (0x4BAC60): restrict true = blacklist, false =
whitelist. The class check passes when the base class or the promotion is
listed. The discipline check is against the discs already held, using the
new rune's list only.
Parity (offline, scripts/p25a.py with feasible race/promotion pairs under
the client rule; scripts/p25b.py over all 315 rune bases). Value changes,
all decided JSON (D6, 2026-10-21):
- 3001 Animator: + Irekei (4 pairs); Summoner blocks it
- 3008 Commander: Saboteur blocks it
- 3010 Duelist: level 20, 5 races, 7 promotions (had no DB rows; -111 pairs)
- 3018 Enchanter, 3030 Conjurer: + Vampire (4 pairs each)
- 3020 Summoner: - Thief/Warrior/Barbarian (28 pairs), + Centaur Druid/Priest
(4); Animator blocks it
- 3031 Darksworn: + Irekei Priest (2); 3032 Valkyr: + Human/Aelfborn Warrior (2)
- 3037 Artillerist: + Sentinel (11); 3044 Strigoi: Belgosch blocks it
- 252127 Master of Throwing: - Scout (16 pairs)
- 70 more runes with no DB rows gain their JSON lists (class runes 2500-2526
except the Pet rune, creation talents and Bloodline talents); only runestones are checked
- Stat chains: the DB "higher rank held" blocks leave the discipline map.
Ranks 1-8 are still refused by the sub type rule; a rank-0 rune now
replaces a higher rank as the client does (decided: follow the client)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the client's ArcCharacter_AddRune (0x4695D0, specs/RuneRequirements.md):
a talent replaces a held talent with the same non-zero rune_sub_type when its
rank is 0 or higher than the held rank; otherwise the apply fails. With no
talent to replace, a new talent needs fewer than 8 held talents.
RuneBase gets runeType, a sub type token (SBStringHash(rune_sub_type), or the
stored hash) and rank from the rune. It no longer copies the static_rune_runereq
type 5 overwrite list, and ApplyRuneMsg no longer reads it.
Value changes:
- Stat chains (*MaxIncrease) replace the same ranks as before; the DB typos go
away (250026 listed 250019 twice and missed 250018)
- Talents in the same group now replace each other when applied from a
runestone (e.g. Bloodline sub-race talents); before, both were added
- Limit: 8 talents instead of more than 12 runes of any kind
- Rank-0 downgrade (decided: follow the client) takes effect in the next
commit, when the DB stat-chain blocks leave the discipline map
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dev commands, BuildingManager, ChatManager, ForgeManager, NPCManager,
NPC, Warehouse, WorkOrder and ManageNPCMsg now call
CObjectTemplate.lookup instead of ItemTemplate.templates.get. Forge
production cost reads getItemResourceCosts(). Only the DB loader and
linkCacheTemplates still use the legacy class (removed in Task 1.8).
Removes the static_item_templates ParityCheck comparator. Final parity
(offline p17.py, 7586 rows): every id has a cache template, 0 item_type
differences, 0 item_resource_costs differences (84 ITEMs have costs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item.template is now the cache template from CObjectTemplate.lookup
instead of the legacy DB ItemTemplate, so item.template.cacheTemplate.X
becomes item.template.X. MobLoot, LootManager, ItemManager.isTwoHanded/
isShield and CombatManager.getSwingAnimation take a CObjectTemplate.
No value changes: every getter already read the cache template through
the bridge, and the lookup resolves the same 7586 ids as
static_item_templates with the same item_type (offline parity p17.py,
0 differences).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LOAD_TEMPLATE_MODTABLES now returns a templateID -> modTable map that
ForgeManager owns (vendorModTables). ForgeManager.forgeItem, WorkOrder
validation and the ManageNPCMsg forge item list read it through
ForgeManager.getVendorModTable(), and ItemTemplate.modTable is removed.
Values are unchanged: static_vendor_items has 5089 rows, one per
templateID, all present in static_item_templates, modTable 0-30.
Templates not in the table still read 0. The old loader stopped at the
first templateID missing from ItemTemplate.templates; there are none.
No comparator added or removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers of the legacy ItemTemplate item_wt, item_value and
item_base_name now read getItemWt(), getItemValue() and
getItemBaseName() on template.cacheTemplate.
Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_wt, item_value and item_base_name. No value changes expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every consumer of the legacy ItemTemplate item_type now reads
template.cacheTemplate.getItemType(). Left on the legacy field: the
type count in dbItemHandler.LOAD_ITEM_TEMPLATES, which runs before the
bridge is linked. The charter checks already read DeedTemplate.
Parity (offline, static_item_templates vs the template
CObjectIndex.lookup picks, scripts/p14.py, 7586 templates: ITEM 4011,
DEED 880, RUNE 2695): 0 differences in item_type. No value changes
expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GuildCreationFinalizeHandler and UseCharterMsg now match charters by
DeedTemplate._deeds instead of the shared item_type getter, like the
other deed paths.
Value change: template 545 "Charter" (ITEM folder, CHARTER) no longer
counts as a charter. It is a base template that players can't get: its
only DB reference is static_vendor_items with vendorType 0, it has no
loot rows, and it is not in GuildCharterType.templateLookup, so guild
creation already rejected it.
JSON-Refactor-Plan Task 1.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deed placement (ObjectActionMsgHandler, PlaceAssetMsgHandler) reads
deed_structure_id from DeedTemplate._deeds. The deed, employment
contract and charter type checks (PlaceAssetMsgHandler,
ActivateNPCMsgHandler, GuildCreationFinalizeHandler, UseCharterMsg,
Item.serialize) read item_type through the cache template bridge.
Charters go through the shared getter rather than _deeds because
template 545 "Charter" is a CHARTER in the ITEM folder.
Parity (offline, static_item_templates vs COBJECTS, scripts/p13.py,
881 templates: all 880 DEED plus ITEM 545): 0 differences in
item_type and deed_structure_id. Every DB DEED, EMPLOYMENTCONTRACT,
PETTOTEM and FURNITUREDEED id is in the DEED folder. No value changes
expected. No pet totem or furniture deed consumer reads template
fields, so neither changes.
JSON-Refactor-Plan Task 1.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>