docs(research): #9 sweep acclient_function_map.md against PDB symbols

Pure-docs sweep. Cross-checked 63 hand-curated entries in
acclient_function_map.md against docs/research/named-retail/symbols.json
(the PDB-derived authoritative name table) using the new helper at
tools/pdb-extract/check_function_map.py.

Findings:
  - Zero entries matched address-and-name exactly. Confirms the
    PDB build is from a different revision than the binary that
    produced our Ghidra chunks (~0x800-0xC10 byte delta varies by
    function cluster). Match by NAME, not by raw address.
  - 38 entries corrected by PDB name lookup. The "Was" column
    preserves the old address for traceability against existing
    code comments. Old entries pointed mid-body of the actual
    function; new column heads point to function starts.
  - 25 entries have no PDB match. Either inlined / non-public
    (no S_PUB32 record) or our hand-derived names were synthesized
    from call-site analysis and don't match the MSVC mangled form
    in the PDB. Several had wrong class assignments (e.g. 0x5387C0
    claimed as CTransition::find_collisions, actually
    CPolygon::polygon_hits_sphere). Flagged for re-derivation in
    acclient_2013_pseudo_c.txt.

Pattern: kept the table format with two address columns (PDB +
legacy) so existing code references using the old addresses can
still be looked up. Added a sweep-summary section at the bottom of
the file documenting the methodology + findings.

Helper script at tools/pdb-extract/check_function_map.py is reusable
for future re-runs (re-run after every PDB regeneration / function
map edit).

Closes #9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-25 17:44:07 +02:00
parent 567078803f
commit 83b020499b
3 changed files with 296 additions and 96 deletions

View file

@ -0,0 +1,149 @@
"""Cross-check acclient_function_map.md addresses against symbols.json.
For each row, looks up the function name in the PDB symbols table and
flags mismatches (mid-body addresses, wrong addresses, missing names).
Run with:
py tools/pdb-extract/check_function_map.py
Used to close ISSUES.md #9 (address-correction sweep).
"""
import json
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent.parent
SYMBOLS = REPO / "docs" / "research" / "named-retail" / "symbols.json"
# (claimed_address, qualified_name) tuples from acclient_function_map.md.
# Format: lowercase hex no leading 0, then the demangled Class::Method.
ENTRIES = [
# CPhysicsObj
("515020", "CPhysicsObj::update_object"),
("5111d0", "CPhysicsObj::UpdatePhysicsInternal"),
("511420", "CPhysicsObj::calc_acceleration"),
("511ec0", "CPhysicsObj::set_velocity"),
("511fa0", "CPhysicsObj::set_local_velocity"),
("511de0", "CPhysicsObj::set_on_walkable"),
("511560", "CPhysicsObj::report_collision_start"),
("513ac0", "CPhysicsObj::report_collision_end"),
("513b60", "CPhysicsObj::handle_obj_collision"),
("515280", "CPhysicsObj::handle_collision"),
("513730", "CPhysicsObj::UpdatePositionInternal"),
("50f940", "CPhysicsObj::calc_friction"),
("510080", "CPhysicsObj::check_contact_velocity"),
# CMotionInterp
("5286b0", "CMotionInterp::get_jump_v_z"),
("528660", "CMotionInterp::jump_charge_is_allowed"),
("5285e0", "CMotionInterp::motion_allows_jump"),
("528ec0", "CMotionInterp::jump_is_allowed"),
("528cd0", "CMotionInterp::get_leave_ground_velocity"),
("529390", "CMotionInterp::jump"),
("529710", "CMotionInterp::LeaveGround"),
("5296d0", "CMotionInterp::HitGround"),
("528960", "CMotionInterp::get_state_velocity"),
("528a50", "CMotionInterp::StopCompletely"),
("5287f0", "CMotionInterp::adjust_motion"),
("5293f0", "CMotionInterp::apply_raw_movement"),
("529210", "CMotionInterp::apply_current_movement"),
("528dd0", "CMotionInterp::contact_allows_move"),
("528f70", "CMotionInterp::DoInterpretedMotion"),
("529080", "CMotionInterp::StopInterpretedMotion"),
("529140", "CMotionInterp::StopMotion"),
("529930", "CMotionInterp::DoMotion"),
("529a90", "CMotionInterp::PerformMovement"),
# CLandBlockStruct
("531d10", "CLandBlockStruct::IsSWtoNECut"),
("532a50", "CLandBlockStruct::ConstructPolygons"),
("532eb0", "CLandBlockStruct::ConstructUVs"),
("532d10", "CLandBlockStruct::unpack"),
("531f10", "CLandBlockStruct::get_packed_size"),
("532440", "CLandBlockStruct::AdjustPlanes"),
("532290", "CLandBlockStruct::CalcCellWater"),
# CLandBlock
("530690", "CLandBlock::Init"),
("5307e0", "CLandBlock::release_all"),
("531780", "CLandBlock::init_static_objs"),
("531000", "CLandBlock::release_visible_cells"),
("5301e0", "CLandBlock::grab_visible_cells"),
("530650", "CLandBlock::add_server_object"),
# LandDefs
("5aaa30", "CLandDefs::get_vars"),
("5aabb0", "CLandDefs::get_outside_lcoord"),
("5aac70", "CLandDefs::AdjustToOutside"),
("5aab50", "CLandDefs::get_block_dir"),
# Collision / Transition (some entries are CCylSphere / CSphere / CTransition / CPolygon)
("5384e0", "CSphere::SphereIntersectsRay"),
("539500", "CPolygon::sphere_intersects_poly"),
("539750", "CPolygon::sphere_intersects_solid"),
("539ba0", "CPolygon::find_time_of_collision"),
("539df0", "CPolygon::find_time_of_collision"),
("53a040", "CPolygon::find_walkable_collision"),
("539110", "CPolygon::calc_normal"),
("539060", "CPlane::ray_plane_intersect"),
("538eb0", "CSphere::slide_sphere"),
("538f50", "CSphere::land_on_sphere"),
("538180", "CTransition::collide_with_point"),
("5387c0", "CTransition::find_collisions"),
("53a230", "CPolygon::hits_walkable"),
# PhysicsEngine
("452a10", "PhysicsEngine::update"),
]
def main():
with open(SYMBOLS, "r", encoding="utf-8") as f:
symbols = json.load(f)
# Build name -> [addresses] (some names appear multiple times: overloads)
by_name = {}
for s in symbols:
by_name.setdefault(s["name"], []).append(s["address"])
# Build address -> name (lowercase hex without 0x prefix)
by_addr = {}
for s in symbols:
addr = s["address"][2:].lower().lstrip("0") or "0"
by_addr[addr] = s["name"]
confirmed = 0
addr_corrections = []
name_misses = []
for claimed_addr, claimed_name in ENTRIES:
# Look up by name
if claimed_name in by_name:
pdb_addrs = by_name[claimed_name]
normalized_claimed = claimed_addr.lower().lstrip("0") or "0"
match = any(
(a[2:].lower().lstrip("0") or "0") == normalized_claimed
for a in pdb_addrs
)
if match:
confirmed += 1
else:
addr_corrections.append((claimed_addr, claimed_name, pdb_addrs))
else:
# Name not found exactly. Try the claimed address as fallback.
normalized_claimed = claimed_addr.lower().lstrip("0") or "0"
actual_name = by_addr.get(normalized_claimed)
name_misses.append((claimed_addr, claimed_name, actual_name))
print(f"Confirmed (address + name match): {confirmed}/{len(ENTRIES)}")
print()
if addr_corrections:
print("ADDRESS CORRECTIONS (name correct, address wrong — likely mid-body):")
for claimed_addr, name, pdb_addrs in addr_corrections:
print(f" 0x{claimed_addr.upper()} {name}")
print(f" actual: {', '.join(pdb_addrs)}")
print()
if name_misses:
print("NAME MISMATCHES (name not found by exact match in PDB):")
for claimed_addr, name, actual_name in name_misses:
print(f" 0x{claimed_addr.upper()} {name}")
if actual_name:
print(f" PDB at 0x{claimed_addr.upper()}: {actual_name}")
else:
print(f" PDB has no symbol at 0x{claimed_addr.upper()}")
print()
main()