# High Noon — training, motion, shop evidence Sources: original IPA `/workspace/highnoon-ipa/Payload/HighNoon.app` and `HighNoon_Clutch_Ghidra.macho.c`. This is research only. No claim of complete fidelity or original-server behavior. Line numbers refer to the C export. Raw numbered extracts are in `extracts/`. Store property list decoded losslessly into `store-decoded.json`. ## Important differences from requested assumptions - The inspected original default lobby enters training by clicking a **scarecrow**, not a gun icon. `SaloonCharactorController::loadSubview` 197431–197480 resolves theme key `scarecrow` and sets action `showPractice:`. The default theme mapping 218251–218252 maps it to `scarecrow-in-duel.png`; no evidence for a gun training shortcut was found. Do not invent a gun icon placement. - The inspected input path uses **accelerometer** data (`CMMotionManager.startAccelerometerUpdatesToQueue:withHandler:`), not a gyroscope attitude stream (207973–208023). A faithful browser port should derive tilt from gravity-including acceleration and the original transforms. A browser motion-permission prompt is a platform adaptation, not an original screen. - Clicking the player's avatar invokes `OppListRootHNCtl::onAvatar:event:` (258717–258827), animates a floating player view to origin (0,48) over .2 seconds, and invokes `MyDetailTabHNCtl::onShowMyDetails:` for the local player. It does **not** directly instantiate DressingRoom in this handler. Following the full chain confirms that this ultimately opens the shop: `onShowMyDetails:` calls `setHidden:1` (254618), custom `MyDetailTabHNCtl::setHidden:` (254263) calls moveDownAnimation, its animation callback `onMoveDownAnimationFinished:finished:context:` (254733) schedules navToDressingRoomScreen, which pushes DressingRoom (254789–254801). Thus player tap opens shop with a staged player/status-panel transition, rather than an intervening user action. ## Training flow, positions, sizes `OppListRootHNCtl::showPractice:` (257959–257974) plays click at 0.3 * sound setting, blocks current view, calls gotoPractice. `gotoPractice` 257930–257954 waits when myGuns is empty; otherwise replaces bottom controller with PracticeHNCtl using transition 1. `PracticeHNCtl::viewWillAppear:` 251978–251990 blocks and invokes `onThreeStaticTargets` on main thread. `onThreeStaticTargets` 251994–252096 creates DuelGame with local player, no opponent, selected dressed Weapon or defaultGunSlug, selected Gun from myGuns; constructs TPlayerData with max_hp=hp=player.life; calls target helper, creates DuelScene under DirectorHNCtl, replaces controller transition 1, pauses menu audio. `FUN_00169e90` 252099–252145 creates exactly **three TargetNode objects**: | target | float X bits | normalized X | Y bits | Y | |---|---|---|---|---| | left | 0x3eaaaaab | float32(1/3) | 0x43c80000 | 400 | | center | 0x3f000000 | .5 | 0x43c80000 | 400 | | right | 0x3f2aaaab | float32(2/3) | 0x43c80000 | 400 | `GameWorldLayer::init` 208626–208686 iterates targets and multiplies target.position.x by worldSize.width; leaves Y as target position Y, adds them at z=1. Background `initWithFile:rect:` at 208608–208615 uses the theme duel_bg and a **1000 by 1000** rect (0x447a0000). Default theme maps to `duel-bg.pvrtc4.pvr` (218252). Thus target world X is float32(1/3)*1000, 500, float32(2/3)*1000. Do not confuse world-space positions with viewport coordinates. `TargetNode::init` 217515–217568 resolves theme scarecrow (`scarecrow-in-duel.png` default), sets figure scale=.5 (0x3f000000), mipmaps, anchorPointInPixels=(imageWidth*.5,0), adds to target node. Original PNG is **146x286**, so visible default figure is 73x143 game units, all three equal; there is no evidence in this static-target initializer for three different depth sizes. The center of the default target hit area is theme point **(74,139)** (0x42940000,0x430b0000, 218245), not the image center. Hit test converts hit to local image space and accepts distance<40 (217637–217678), scores 10-trunc(distance*10/40), max score tracked. Successful target calls CCScaleTo(duration=.2,scaleX=1,scaleY=.2), ease-in rate 3, die. All targets dead shows localized FinishPractice dialog with redirect to OppListScene (217573–217598). ## Camera / motion: what is established - GameViewportLayer.init 207795–207919 creates a **mover node containing GameWorldLayer**. Its step changes mover.position; it also applies world.viewScale to viewport. This establishes moving the entire room, rather than only a crosshair. - `worldSize` 208500–208514 returns background.contentSize. - GameWorldLayer.init centerPoint 208689–208704 uses half world width and screenCenterY + .5*(worldHeight-480). This depends on screen center globals; raw Mach-O globals can resolve the original geometry. - `startAccelerometerUpdates` 207973–208010 uses serverConfigs.accelerometerUpdateInterval. **Value is not present in this function.** - `updateAccelerometerData:` 208299–208435: low-pass filters x/y/z using global coefficient, adds optional shakingAccelVal noise, stores filtered acceleration globals, converts serverConfigs.tiltBalancedAngle degrees to radians, forms cursor from negative x and a sin/cos mixture of y/z. When aimingMode=0, applies an additional weapon-weight low-pass with weight clamped [0,1] and serverConfigs.gunWeightLowPassRange. Exact ARM float reconstruction should be checked before translating vector expressions. - `cursorToAttitudeUsingGunMode:` 208038–208070 reads tiltAngleRange and multiplies cursor components by 90/range components, then clamps both [-1,1]. - `cursorToAttitudeUsingAccMode:` 208077–208113 accumulates cursor*accModeSensitivityK*10*dt into attitude and clamps [-1,1]. dt register is reconstructed as in_r2; validate ABI/disassembly for literal translation. - `step:` 208120–208290 selects gun or acc mapping using aimingMode, rotates attitude using viewport rotation, multiplies by flipAxis and screenShift, clamps world-coordinate camera center using worldRect/worldInsets and world rotation, converts back to mover coordinates, and sets **mover position = screenCenter - converted camera center**. Set viewport scale to world.viewScale. Exact clamping/inset values depend on original screen globals and server settings. - `GameWorldLayer::step:` 209326 sets low-pass coefficient **0.1/(viewScale*viewScale)**. Previous reports misattributed this line to GameViewportLayer; it belongs to GameWorldLayer. ### Recovered original default motion parameters Further tracing `ServerConfigs::init` 312650–312747 recovered **client defaults**, subject to server overrides via updateWithServerSettings 312757: | property | value | source bits / lines | |---|---|---| | attitudeMappingWidthRange | 340 | 0x43aa0000,312668 | | defaultAimingMode | 0 (gun mode) | 312678 | | gunModeReloadAttitudeY | -0.8 (float32) | 0xbf4ccccd,312695 | | accModeReloadAttitudeY | -0.5 | 0xbf000000,312663 | | gunWeightLowPassRange | [.01,.2] doubles | 0x3f847ae147ae147b and 0x3fc999999999999a,312697–312700 | | tiltAngleRange | [22.5,30] degrees | 0x4036800000000000 and 0x403e000000000000,312723–312726 | | tiltBalancedAngle | 45 degrees | 0x4046800000000000,312729 | | accelerometerUpdateInterval | float32(1/60) | 0x3c888889,312746 | These permit an evidence-based default implementation; historic production overrides are still unknown. Selected-gun weight and user settings (axis flip, sensitivity, aiming preference) remain runtime data. Exact scalar camera transformations still require validating ambiguous vector/ABI reconstruction. ## Shop and dressing resources `DressingRoomHNCtl::initWithTarget:` 262748–262761 loads original **DressingRoom.nib**. `viewDidLoad` 262801–262926 uses: - background `room-background.png` (320x480), left `room-background-left.png` (135x480) - shadow `room-items-background-shadow.png`, stretch caps 16,16 - `room-icon-wampum.png`, localized WAMPUM label - get-more button `room-button-get-more-wampum-text.png` composited on `btn-purple-w98.png` - existing floating PlayerView reused if matching local player; otherwise PlayerView at **(0,48)** (262905–262916) - `GoldWidget.initWithY:` **390** (0x43c30000,262919) - wardrobe comes from `store.plist` `female_wardrobe` or `male_wardrobe` by player's is_female, then pushToItem target slug. `store.plist` decodes successfully; **988 item metadata records**, `dressing`, `cell-layouts`, `templates`, male/female category trees. Full structure preserved in `store-decoded.json`. Root categories in both gender trees: expression, general, clothes, guns, indian, barber, followed by original layout markers and buttons. Examples of exact layout records: | layout | item width x height | columns | left | top | horizontal | vertical | bottom | |---|---|---|---|---|---|---|---| | categories | 182x49 | 1 | 10 | 20 | 0 | 10 | 0 | | three | 54x54 | 3 | 10 | 10 | 6 | 6 | 10 | | two | 84x54 | 2 | 10 | 20 | 6 | 10 | 10 | | clothes | 55x55 | 3 | 10 | 50 | 5 | 5 | 0 | | barber | 184x57 | 1 | 10 | 50 | 0 | 30 | 0 | Read `StoreWardrobe::addItemGroups` 277801 and `addGroupsWithLayout:itemInfos:` 277880 to implement marker grouping correctly. DressingRoom creates StoreWardrobe with original NIB window.bounds, so NIB exact window frame remains necessary. Assets physically present include `room-category-guns.png`, `room-category-western-wear.png`, `room-category-indian.png`, `room-category-barber.png`, gender-specific expression icons, all room-subs category assets, `room-items-lock.png`, `room-items-mark.png`, `room-items-quantity.png`, `room-items-buy-bg.png`, `room-icon-dollar.png`, `room-icon-wampum.png`, original dress layers and weapon atlases. `StoreItemCell::update` 279087–279218 distinguishes static metadata `plistItemsDict` from server TItem records `tItemsDict`. Missing tItemsDict **blocks cell**; required_level and owned_qty etc come from server object. Locked if player.level etc. `getImageSuffixWithItemInfo:state:layer:` 158618–158678 adds weapon-dependent suffixes. The required defaults (store.plist dressing) are: Skin=skin1, Body male body1/female body2, Shirt male shirt-1/female shirt-female-02, Pants male jeans-1/female pants-female-02, Boots male boot1/female boots-female-11, Face male face1/female face-female-2, Fur fur1, Eyes male eyes-normal/female eyes-female-normal, Mouth male mouth-1/female mouth-female-normal, female Hair=hair-female-bargirl, Weapon=revolver. Skirts/dresses hide/exclude other layers per plist. Colors/masks also require the original compositing path; do not flatten into a different invented character. PlayerView overall base is 122x362 (198513); body rendering base **122x322**. `FUN_0013e970` 218200–218213 returns X scale1, Y=.9+clamp(level*.1/20,0,.1). PlayerView.transformedPlayerSize 198738 multiplies 122x322 by those scales; getBodyRect 198770 sets x0, y=322-transformedHeight. Thus player height depends on original player level, not an arbitrary CSS size. ## Unresolved - NIB exact rectangles/outlets are handled by separate parent analysis; this report does not substitute guessed values. - Production overrides of recovered client-default motion settings, selected-gun weight, and authoritative shop price/availability data. - Final coordinate semantics in float/vector code and global screen center/scale values require Mach-O/disassembly validation where indicated. - Original default saloon avatars XML can be replaced by server settings (`saloon_avatars`), so default order is not proof of every production session's ordering. ## ARM binary verification addendum Installed Capstone in `/workspace/highnoon-tools` and wrote `tools/macho.py`, a Mach-O fat/ARM32 segment + Objective-C class/method parser. It reads the original IPA executable, obtains method IMP addresses directly, and disassembles original Thumb instructions. `objc-method-addresses.json` lists original method addresses. Raw disassembly retained in extracts/motion-arm.txt, camera-arm.txt, camera-init-arm.txt, lobby-background-arm.txt. No execution of the original binary. ### Correct original screen globals Direct original Mach-O bytes: | C export pointer | points to | decoded value | |---|---|---| | PTR_DAT_003f124c | 0x3f0688 | CGPoint **(160,200)** | | PTR_DAT_003f1250 | 0x4b3770 | float32 .1 initial filter coefficient | | PTR_DAT_003f1254 | 0x3f0678 | float32 .36 opponent base scale | | PTR_DAT_003f125c | 0x3f067c | float32 426 opponent base Y | The aim/screen point is **not the geometric center (160,240)**. In a 320x480 bottom-origin Cocos canvas it is (160,200), or DOM (160,280). ### Verified default scalar camera equations For world rotation=0, viewport rotation=0 and viewScale=1, original code reduces to the following. These conditions describe the initial unrotated state, not all zoom/recoil states. - Filter each acceleration axis: a = filterK*sample + (1-filterK)*previous. Original input is acceleration in units of g. For a browser accelerationIncludingGravity stream in m/s², unit conversion and sign/orientation must be validated on actual iPhone Safari; do not merely substitute deviceorientation angles. - balance=45 degrees by default. ARM 0x12f1cc–0x12f226 proves raw cursor.x=-filteredAx; raw cursor.y=filteredAz*cos(balance)-filteredAy*sin(balance). - Optional shake adds uniform noise per acceleration axis (if shakingAccelVal>0) before cursor conversion. - Gun mode weight low-pass: w=clamp(gun.weight,0,1); k=.01+(1-w)*(.2-.01). Cursor = k*rawCursor+(1-k)*previousCursor. Gun weight value remains unverified runtime data, so do not pick one arbitrarily. - gun attitude = (clamp(cursor.x*90/22.5,-1,1), clamp(cursor.y*90/30,-1,1)). ARM 0x12ead2–0x12eb42 confirms component order and clamps. - Acc mode attitude += cursor*accModeSensitivityK*10*dt, component clamp[-1,1]. ARM 0x12eb94–0x12ec08 confirms dt preserved in r5 and both components. ControlPad::init C205255–205350 defaults flipAxis=(1,1), sensitivity .5 if persisted value outside [.01,1], defaultAimingMode0. - Initial world rect (0,0,1000,1000), screenPoint (160,200). - world centerPoint=(worldWidth/2,screenPoint.y+(worldHeight-480)/2) = **(500,460)**. - screenShift=(min(centerPoint.x-screenPoint.x,340),centerPoint.y-screenPoint.y) = **(340,260)**. - xInset=max(screenPoint.x,320-screenPoint.x)=160; yInset=max(screenPoint.y,480-screenPoint.y)=280. ARM camera-init 0x12e712–0x12e77e proves max calculations and stored order (160,280,160,280). - desired camera center = centerPoint + attitude*flipAxis*screenShift. - clamp camera x to[160,840] and camera y to[280,720]. - mover.position = screenPoint - cameraCenter. At attitude(0,0), mover=(-340,-260); at attitude(1,1), mover=(-680,-520); at attitude(-1,-1), mover=(0,-80). - The mover contains **the full world**, so background and targets move together. The viewport anchorPointInPixels is converted screenPoint, and zoom applies to GameViewportLayer itself, keeping that point as its transform anchor. At nonzero rotation the original `step:` first rotates attitude with CGAffineTransformMakeRotation(viewport.rotation*pi/180), applies flipAxis and screenShift, adds centerPoint in mover coordinates, converts mover->world, clamps rotated-world limits, converts world->mover, then subtracts from screenPoint. World insets transform with abs(sin(worldRotation)) and abs(cos(worldRotation)). The raw ARM sequence 0x12edb4–0x12ee7e confirms their ordering. Preserve this node-space approach for full port; initial scalar simplification alone does not cover rotated effects. ### Lobby background exact positions ARM `OppListRootHNCtl::viewDidLoad` 0x17054c–0x1705c0 resolves misleading decompilation around its setFrame calls. Stack struct sp+0x94 is land frame, y at sp+0x98; sp+0x84 is house frame, y atsp+0x88. It computes land.y=480-land.image.height; house.y=land.y-house.image.height. Original default assets are land960x363, house960x27, sky960x129. Thus **land y117, house y90, sky y0**, all x0 initially, native image sizes. House is inserted above sky, land above sky (house inserted after land so behind land per insert-above-sky ordering). This is image-layer assembly, not a flat guessed background.