//===========================================================================// // File: btl4rdr.cpp // // Project: BattleTech Brick: Gauge Renderer Manager // // Contents: MapDisplay -- the cockpit radar / threat-map gauge. Draws a // // top-down (X/Z) map centred on the operator mech, plots blips for // // nearby static and moving entities, the player's view wedge, and // // floating name labels for tracked mechs. // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // 09/13/95 CPB Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1994, Virtual World Entertainment, Inc. All rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (BTL4OPT.EXE). Behaviour follows the // Ghidra pseudo-C captured in recovered/all/part_013.c; the class/member names // are taken verbatim from the SURVIVING header BTL4RDR.HPP, whose member // byte-offsets match the decompiled object exactly: // // offsetPosition @0x2D0 backgroundColor @0x358 // currentScale @0x2D4 staticColor @0x35C // currentPositionPointer @0x2D8 boxColor @0x360 // currentAngularPointer @0x2DC halfWidth @0x364 // currentCapabilitiesRatio@0x2E0 halfHeight @0x368 // LODIndex @0x2E4 operating @0x36C // pixelsPerMeter @0x2E8 rockAndRoll @0x370 // metersPerPixel @0x2EC flashState @0x374 // maximumRange @0x2F0 staticEntityList @0x378 // maximumDistanceSquared @0x2F4 movingEntityList @0x394 // xMin/yMin/zMin @0x2F8.. previousDrawing @0x3B0 // xMax/yMax/zMax ..0x30C halfViewWidthInUnits@0x3C0 // viewingPosition @0x310 shadowTable @0x3C4 // viewHorizontalRotation @0x31C nameArray[16] @0x090 (stride 0x24) // viewHalfHorizontalWidth @0x320 // worldToView @0x324 (base GraphicGauge: renderer@0x1C, // embedded GraphicsView@0x48) // // --------------------------------------------------------------------------- // PORT NOTE. This module is compiled against the surviving WinTesla (RP411) // MUNGA / MUNGA_L4 engine headers. Several facilities the original BattleTech // radar leaned on are BT-specific and are NOT present in that engine: // * the per-Entity "video object" (a name-bitmap id + current target), // * the radar "pip" symbol table, // * the gauge-renderer's spatial worlds, operator-entity connection and // pip table, // * the global name-bitmap cache (the DAT_004efc94 App singleton). // They are reproduced as a file-local adaptation layer below so the recovered // MapDisplay logic keeps its full structure and compiles; the bodies are // best-effort and currently resolve to "unavailable" in this engine build // (see the reconstruction CROSS-FAMILY NEEDS). All engine 2D/matrix calls // have been retargeted to the real WinTesla API names: // GraphicsView::MoveTo->MoveToAbsolute, LineTo->DrawLineToAbsolute, // Blit->DrawBitMap, BeginClip->AttachRecorder, EndClip->DetachRecorder, // SetExtent->SetPositionWithinPort; GraphicsViewRecord::Erase->Draw, // Reset->Clear; AffineMatrix::Identity->BuildIdentity, // SetTranslation->SetFromAxis(W_Axis), Concatenate->operator*=, // Scale->Multiply(m,v), FromQuaternion->operator=, Transform->point*=m; // BitMap width/height -> Data.Size.x/.y; the embedded GraphicsView is the // base GraphicGauge::localView. // --------------------------------------------------------------------------- // #include // task #17 (radar symbology): the "pip" system is the ENGINE's L4GaugeImage // (vector-stroke shapes, resource type 0x12) cached in L4Warehouse:: // gaugeImageBin; name labels are the Mission's prebuilt egg bitmaps. #include // L4GaugeImage -- the pip vector shapes (fwd shim) #include // L4Warehouse::gaugeImageBin -- the pip table #include // Mission::GetSmallNameBitmap -- the name labels #include // Player::playerBitmapIndex #pragma hdrstop #if !defined(BTL4RDR_HPP) # include #endif #if !defined(APP_HPP) # include #endif #include #include #include // // Tuning constants recovered from the CODE constant pool. // static const Scalar CalcBoundsEpsilon = 0.0f; // _DAT_004c2284 static const Scalar MetersPerPixelNum = 1.0f; // _DAT_004c2288 static const Scalar OperatingEpsilon = 0.0f; // _DAT_004c2174 static const Scalar HeadingHalf = 0.5f; // _DAT_004c2480 (half-angle) static const Scalar ViewWedgeRadius = 10000.0f; // _DAT_004c25ac static const Scalar MapVerticalSpan = 32768.0f; // 0x47000000 / 0xc7000000 // // FUN_004c2f88 -- the fixed blip / name colour index used throughout the map. // (Returns the literal 7 in the shipped binary.) // static int MapBlipColor() { return 7; } //########################################################################### //########################################################################### // BT radar adaptation layer (file-local -- see PORT NOTE above) //########################################################################### //########################################################################### // // task #17 DECODED: the "video object" at *(entity+0x190) IS Entity:: // owningPlayer (ENTITY.h:589; resourceID is the adjacent +0x1BC) -- the radar // reads its playerBitmapIndex (+0x1E0, the egg 'bitmapindex' key) for the // name label and BTPlayer::objectiveMech (+0x284) for the target box. The // old invented VideoObject class is retired. // // The "pip" is the ENGINE's L4GaugeImage (L4GAUIMA.h -- FUN_0046f0c0 IS // L4GaugeImage::Draw line-for-line), and the "pip table" is L4Warehouse:: // gaugeImageBin (WarehouseBinOf, warehouse+0x58) reached via // GaugeRenderer::warehousePointer (+0x4C) -- keyed by Entity::resourceID // (BTL4.RES ships 110 type-0x12 vector shapes: every mech/vehicle/building/ // tree). Thin facades keep the recovered draw-loop shape: // class Pip // == L4GaugeImage { public: void Draw( Scalar lod_index, Scalar meters_per_pixel, GraphicsView *view, int color, AffineMatrix &transform, int box_color ) { ((L4GaugeImage *)this)->Draw(lod_index, meters_per_pixel, view, color, transform, box_color); } }; class PipTable // == L4Warehouse { public: Pip * LookUpPip(ResourceDescription::ResourceID entity_resource_ID) { // FUN_004688ce(warehouse+0x58, entity+0x1BC, 0) -- the refcounting // Get, exactly as the binary's per-frame loop calls it (the // mission-end Purge reclaims; one vector shape per model). return (Pip *)((L4Warehouse *)this)->gaugeImageBin.Get( entity_resource_ID, False); } }; // // Entity placement helpers -- the WinTesla Entity keeps its world placement in // localOrigin (linearPosition + angularPosition). These give the radar the // Point3D position and AffineMatrix transform the recovered code expects. // static inline Point3D & EntityPosition(Entity *entity) { return entity->localOrigin.linearPosition; } static inline void EntityTransform(AffineMatrix *out, Entity *entity) { *out = entity->localOrigin; } // // BT-renderer facilities not present on the WinTesla GaugeRenderer. // static Entity * ResolveOperatorEntity(GaugeRenderer *renderer); static PipTable * GetPipTable(GaugeRenderer *renderer); // // Name-label + target resolution (task #17): the label bitmap comes from the // owning Player's bitmapindex through the Mission's SMALL prebuilt-raster // chain; the target from the owning BTPlayer's objectiveMech (bridge). // static BitMap * LookUpNameBitmap(int name_ID); static int GetNameID(Entity *entity); static Logical IsLabelledEntity(Entity *entity); // // The owner mech's current target (via its video object). // static Entity * GetTarget(Entity *owner); // // Mech sub-object lookup ("Torso") + its horizontal rotation feed. // static Entity * FindSubObject(Entity *entity, const char *name); static Radian * GetHorizontalRotation(Entity *entity); //########################################################################### //########################################################################### // MapName //########################################################################### //########################################################################### // // @004c19fc -- snapshot a tracked entity into one name-array slot: project its // world position into the (already scaled) view space, look up its name bitmap // from the global font/name cache, and flag whether it is the hot-boxed target. // void MapName::ExtractFromEntity( Entity *entity, AffineMatrix &worldToView, Entity *exclude_entity, Entity *hotbox_entity ) { // // The operator's own mech (exclude_entity) draws no name. A NULL entity // (the binary's Verify(False) count/iterator-mismatch path, which compiles // out at DEBUG_LEVEL 0) would crash EntityPosition(NULL) below -- guard it. // if (entity == NULL || entity == exclude_entity) { nameBitmap = NULL; // param_1[6] = 0 return; } Player *owning_player = entity->GetOwningPlayer(); // *(entity+0x190) if (owning_player == NULL) { nameBitmap = NULL; // unowned (dummy/effect): no label } else { // player+0x1E0 playerBitmapIndex -> the Mission's SMALL name-raster // chain (prebuilt 64x16 egg bitmaps, loaded 1-based by // Mission::LoadNameBitmaps). nameBitmap = LookUpNameBitmap(owning_player->playerBitmapIndex); } // // Project the entity origin into screen space and stamp the label. // center.Multiply(EntityPosition(entity), worldToView); // FUN_00408b98 color = MapBlipColor(); // 7 hotBoxed = (entity == hotbox_entity); // // The decompiled object always parks the name's pointer direction at // (-1, 0, 0) -- labels are screen-aligned, not entity-aligned. // direction.x = -1.0f; direction.y = 0.0f; direction.z = 0.0f; } // // @004c1ab0 -- blit one extracted name bitmap at its projected screen point, // in either its own colour or (when hot-boxed) the supplied highlight colour. // void MapName::Draw( GraphicsView *graphics_view, int hotbox_color ) { if (nameBitmap == NULL) { return; } graphics_view->MoveToAbsolute(Round(center.x), Round(center.z)); // +0x24, FUN_004dcd94 if (!hotBoxed) { graphics_view->SetColor(color); // +0x18 } else { graphics_view->SetColor(hotbox_color); } graphics_view->DrawBitMap( // +0x54 0, nameBitmap, 0, 0, nameBitmap->Data.Size.x - 1, nameBitmap->Data.Size.y - 1 ); } //########################################################################### //########################################################################### // MapDisplay //########################################################################### //########################################################################### //############################################################################# // Method Description + Make -- the config "map" primitive registration glue. // // NOT in the assert-anchored decomp (the ctor @004c1c24 has no exported caller // and the "map" string was not captured), so this is reconstructed from the // config invocation (L4GAUGE.CFG:4923) + the ctor signature. The offset_position // keyword "center"/"bottom" is typed as a STRING and converted in Make, so it // needs no ModeManager named-constant registration. // MethodDescription MapDisplay::methodDescription = { "map", MapDisplay::Make, { // map( rate, mode, (w,h), offsetPos, viewDeg, bg,static,hotbox, // maxRange, scaleAttr, positionAttr, angleAttr, capabilitiesAttr ) { ParameterDescription::typeRate, NULL }, // rate ID { ParameterDescription::typeModeMask, NULL }, // mode mask { ParameterDescription::typeVector, NULL }, // (width,height) { ParameterDescription::typeString, NULL }, // offset position ("center"/"bottom") { ParameterDescription::typeInteger, NULL }, // view width (degrees) { ParameterDescription::typeColor, NULL }, // background colour { ParameterDescription::typeColor, NULL }, // static/default colour { ParameterDescription::typeColor, NULL }, // hotbox colour { ParameterDescription::typeScalar, NULL }, // maximum range (metres) { ParameterDescription::typeAttribute, NULL }, // scale (RadarRange) { ParameterDescription::typeAttribute, NULL }, // position (RadarLinearPosition, Point3D*) { ParameterDescription::typeAttribute, NULL }, // angle (RadarAngularPosition, Quaternion*) { ParameterDescription::typeAttribute, NULL }, // capabilities (Avionics/RadarPercent) PARAMETER_DESCRIPTION_END } }; Logical MapDisplay::Make( int display_port_index, Vector2DOf position, Entity *entity, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; OffsetPosition offset = (strcmp(p[3].data.string, "bottom") == 0) ? bottom : center; MapDisplay *gauge = (MapDisplay *)operator new(sizeof(MapDisplay)); if (gauge != NULL) { new (gauge) MapDisplay( p[0].data.rate, p[1].data.modeMask, (L4GaugeRenderer *)gauge_renderer, 0, // owner_ID display_port_index, position.x, position.y, // left, bottom position.x + p[2].data.vector.x, // right position.y + p[2].data.vector.y, // top offset, p[4].data.integer, // view width (degrees) p[5].data.color, p[6].data.color, p[7].data.color, // bg, static, hotbox p[8].data.scalar, // maximum range entity, (Scalar *)p[9].data.attributePointer, // scale (RadarRange) (Point3D **)p[10].data.attributePointer, // position (RadarLinearPosition) (Quaternion **)p[11].data.attributePointer, // angle (RadarAngularPosition) (Scalar *)p[12].data.attributePointer); // capabilities (Avionics/RadarPercent) } return True; } //############################################################################# // Construction / Destruction // // @004c1c24 -- vtable 0051422c, base name "MapDisplay". Builds the 16-entry // name array, the static/moving entity lists and the previous-drawing record, // sizes the graphics port from (left,bottom,right,top), allocates the radar // shadow table, and wires the four data-source gauge connections // (scale, position, angle, capabilities) plus an optional torso-rotation feed. // MapDisplay::MapDisplay( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer, unsigned int owner_ID, int graphics_port_number, int left, int bottom, int right, int top, OffsetPosition offset_position, int view_width_in_degrees, int bg_color, int static_color, int hotbox_color, Scalar maximum_range, Entity *entity, Scalar *scale_value_pointer, Point3D **position_pointer, Quaternion **angle_pointer, Scalar *capabilities_ratio, Logical allow_rock_and_roll ): GraphicGauge( // FUN_00444818 rate, mode_mask, renderer, owner_ID, graphics_port_number, "MapDisplay" ) { // // nameArray / staticEntityList / movingEntityList / previousDrawing are // member objects -- the decompiler's explicit Construct_Array / ctor thunks // are the compiler-generated member construction; nothing to do here. // // // Size the graphics port and centre the origin. // localView.SetPositionWithinPort(left, bottom, right, top); // (this+0x48 vtbl+0x08) if (offset_position == MapDisplay::bottom) { localView.SetOrigin((right - left) >> 1, 0); // vtbl+0x10 } else { localView.SetOrigin((right - left) >> 1, (top - bottom) >> 1); } backgroundColor = bg_color; // this[0xD6] staticColor = static_color; // this[0xD7] boxColor = hotbox_color; // this[0xD8] maximumRange = maximum_range; // this[0xBC] rockAndRoll = allow_rock_and_roll; // this[0xDC] halfWidth = (right - left) >> 1; // this[0xD9] halfHeight = (top - bottom) >> 1; // this[0xDA] offsetPosition = offset_position; // this[0xB4] halfViewWidthInUnits = view_width_in_degrees >> 1; // this[0xF0] shadowTable = (ShadowRecord *)operator new(halfViewWidthInUnits * 8); // this[0xF1] viewHorizontalRotation = 0.0f; // this[199] (0x31C) viewHalfHorizontalWidth = 0.3926991f; // this[200] (0x320) = PI/8 flashState = False; // this[0xDD] // // Data-source connections: each is a small heap GaugeConnection wired so // that the gauge framework copies the external value into a member field // every frame. scale -> currentScale, position -> currentPositionPointer, // angle -> currentAngularPointer, capabilities -> currentCapabilitiesRatio. // AddConnection( // (*this+0x34) new GaugeConnectionDirectOf(0, ¤tScale, scale_value_pointer)); // FUN_00474855 AddConnection( new GaugeConnectionDirectOf(0, ¤tPositionPointer, position_pointer));// FUN_004c2a3e AddConnection( new GaugeConnectionDirectOf(0, ¤tAngularPointer, angle_pointer)); // FUN_004c2bc0 AddConnection( new GaugeConnectionDirectOf(0, ¤tCapabilitiesRatio, capabilities_ratio));// FUN_00474855 // // If the host entity is a mech (class id 0xBB9) with a "Torso" sub-object, // feed its horizontal rotation into the map's view heading. // if (entity->GetClassID() == 0xBB9) { Entity *torso = FindSubObject(entity, "Torso"); // FUN_0041f98c if (torso != NULL) { AddConnection( new GaugeConnectionDirectOf( // FUN_004c2d42 0, &viewHorizontalRotation, GetHorizontalRotation(torso) /*+0x1D8*/)); } } } // // @004c1ea8 -- free the shadow table. previousDrawing, the two entity lists // and the name array are member objects and are destroyed automatically after // this body runs (the decompiler's explicit dtor thunks are that automatic // member destruction), so only the heap shadowTable is released here. // MapDisplay::~MapDisplay() { operator delete(shadowTable); // FUN_004022E8(this[0xF1]) // base ~GraphicGauge -- FUN_00444870 } // // @004c1f30 // Logical MapDisplay::TestInstance() const { return GraphicGauge::TestInstance(); // FUN_004448ac } // // @004c1f40 -- emit "ThreatIndicator:" then chain to the base ShowInstance // with a deeper (tab) indent. // void MapDisplay::ShowInstance(char *indent) { std::cout << indent << "ThreatIndicator:"; // FUN_004dbb24 x2 char deeper[80]; strcpy(deeper, indent); strcat(deeper, "\t"); // FUN_004d49b8(local_54, "\t") GraphicGauge::ShowInstance(deeper); // FUN_004448bc } // // @004c1fc0 -- reset the per-frame phase counter when the gauge is shown. // void MapDisplay::BecameActive() { operatingPhase = 0; // this+0x354 } // // @004c1fd0 -- the per-frame work, spread across four ticks (phases 0..3) so // the cost is amortised: // 0 decide whether the radar is powered, compute range, set up bounds // 1 spatial-query static (terrain/structure) entities into the list // 2 spatial-query moving (mech/vehicle) entities into the list // 3 erase the previous frame and redraw the map // void MapDisplay::Execute() { int phase = operatingPhase++; // this+0x354 const int mlog = getenv("BT_MAP_LOG") ? 1 : 0; if (phase == 0) { operating = (OperatingEpsilon < currentCapabilitiesRatio); // 0.0f < ratio if (mlog) DEBUG_STREAM << "[map] p0 operating=" << (int)operating << " cap=" << currentCapabilitiesRatio << " scale=" << currentScale << "\n" << std::flush; if (operating) { // Repopulate the renderer's entity grids (the port dropped the engine's // per-frame InterestingEntity feed, so the within-bounds queries in // phases 1/2 found nothing). Done here, under the gauge's guarded // Execute, so a fault disables only the map -- not the whole renderer. renderer->RebuildEntityGrid(); maximumDistanceSquared = currentCapabilitiesRatio * maximumRange; maximumDistanceSquared = maximumDistanceSquared * maximumDistanceSquared; CalculateBounds(); // FUN_004c2178 if (mlog) DEBUG_STREAM << "[map] p0 bounds ok\n" << std::flush; } } else if (phase == 1) { staticEntityList.Clear(); // FUN_004435ac(this+0x378) if (operating) { renderer->GetStaticEntitiesWithinBounds( // FUN_00443ba4(renderer+0xB0) &staticEntityList, xMin, yMin, zMin, xMax, yMax, zMax); if (mlog) DEBUG_STREAM << "[map] p1 static gathered\n" << std::flush; } } else if (phase == 2) { movingEntityList.Clear(); // FUN_004435ac(this+0x394) if (operating) { renderer->GetMovingEntitiesWithinBounds( // FUN_0044366c(renderer+0x94) &movingEntityList, xMin, yMin, zMin, xMax, yMax, zMax); if (mlog) DEBUG_STREAM << "[map] p2 moving gathered\n" << std::flush; } } else { if (phase == 3) { EraseDisplay(); // FUN_004c2294 if (operating) { DrawDisplay(); // FUN_004c22c4 } if (getenv("BT_MAP_LOG")) { static int s_c = 0; if ((s_c++ % 15) == 0) DEBUG_STREAM << "[map] operating=" << (int)operating << " scale=" << currentScale << " cap=" << currentCapabilitiesRatio << " posPtr=" << (currentPositionPointer ? 1 : 0) << " anglePtr=" << (currentAngularPointer ? 1 : 0) << "\n" << std::flush; } } operatingPhase = 0; } } // // @004c2178 (file=bt/heat... actually bt_l4/btl4rdr.cpp -- CONFIRMED by the // embedded assert path "d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x224). // // Derive the metres<->pixels scale from the current zoom (currentScale) and // build the world-space axis-aligned box that the spatial queries will use. // The map is top-down, so the X and Z bounds hug the viewing position while // the Y (vertical) bounds are opened wide (+/-32768). // void MapDisplay::CalculateBounds() { if (currentScale <= CalcBoundsEpsilon) { Verify(False); // FUN_0040385c // "MapDisplay::CalculateBounds detected a bad scale" // "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x224 currentScale = 1.0f; // 0x3f800000 } pixelsPerMeter = (Scalar)(halfWidth * 2) / currentScale; metersPerPixel = MetersPerPixelNum / pixelsPerMeter; // 1.0 / pixelsPerMeter LODIndex = metersPerPixel; Scalar halfWorldWidth = (Scalar)halfWidth / pixelsPerMeter; Scalar halfWorldHeight = (Scalar)halfHeight / pixelsPerMeter; Entity *owner = ResolveOperatorEntity(renderer); // FUN_00417ab4(renderer+0x40) viewingPosition = EntityPosition(owner); // FUN_00408440(this+0x310, owner+0x100) xMin = viewingPosition.x - halfWorldWidth; xMax = viewingPosition.x + halfWorldWidth; yMin = -MapVerticalSpan; // 0xc7000000 yMax = MapVerticalSpan; // 0x47000000 zMin = viewingPosition.z - halfWorldHeight; zMax = viewingPosition.z + halfWorldHeight; } // // @004c2294 -- erase last frame's footprint and forget it. // void MapDisplay::EraseDisplay() { previousDrawing.Draw(&localView, backgroundColor); // FUN_0044a650 previousDrawing.Clear(); // FUN_0044a630 } // // @004c22c4 -- compose the world->view matrix (translate to the viewing // position, optionally rotate by heading, then scale to pixels), then paint // the wedge, the static and moving blips and the names, recording the touched // region for next frame's erase. // void MapDisplay::DrawDisplay() { AffineMatrix view; view.BuildIdentity(); // FUN_0040aadc (twice in decomp) view.BuildIdentity(); // // Default the centre to the operator mech if no explicit feed is wired. // Entity *owner = ResolveOperatorEntity(renderer); if (currentPositionPointer == NULL && owner != NULL) { currentPositionPointer = &EntityPosition(owner); // owner+0x100 } if (currentPositionPointer != NULL) { Point3D center = *currentPositionPointer; // FUN_00408440(local_5c, ...) // TRANSCRIPTION FIX #2 (the invisible-pip bug): FUN_0040adec writes // ONLY the 3x3 rotation elements ([0..2],[4..6],[8..10]) -- it never // touches the translation row. The binary therefore ends with // view = [R(q) | center]: rotation SET, center PRESERVED. Our old // `view *= yaw` was the engine's full COMPOSE, which also rotated the // translation (view = [R | center x R]) -- the subsequent Invert then // mapped center x R to the origin instead of the viewer, so every // blip landed hundreds of pixels off-scope. Rotation first // (rotation-only assignment), translation row LAST. if (currentAngularPointer != NULL) { if (!rockAndRoll) { // // Top-down: keep heading only. Extract the yaw, build a // yaw-only quaternion, SET the rotation block from it. // EulerAngles euler; euler = *currentAngularPointer; // FUN_0040954c Scalar heading = euler.yaw * HeadingHalf; // * 0.5f SinCosPair sc; sc = Radian(heading); // FUN_00408328 Quaternion yaw(0.0f, sc.sine, 0.0f, sc.cosine); // FUN_00409948 view = yaw; // rotation-only (FUN_0040adec semantics) } else { // // "Rock and roll": use the full orientation. // view = *currentAngularPointer; // rotation-only build } } view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center) -- translation LAST } // TRANSCRIPTION FIX (the invisible-pip bug, 2026-07-12): FUN_0040b244 is // the full affine INVERSE (cofactor expansion + determinant divide, // part_001.c:172), not a copy. `view` above is the OPERATOR's local-> // world pose (translate to the mech + yaw); the radar needs its inverse // -- world->scope, center subtracted, heading unrotated. Read as a copy, // every pip/name transformed to (world + center) x ppm = thousands of // pixels off-scope: the scope drew empty at all times. worldToView.Invert(view); // FUN_0040b244(this+0x324, view) // // Bake the metres->pixels scale into the matrix. // Vector3D scale; scale.x = scale.y = scale.z = pixelsPerMeter; worldToView.Multiply(worldToView, scale); // FUN_0040b374 if (getenv("BT_MAP_LOG")) { static int s_m = 0; if ((s_m++ % 30) == 0) DEBUG_STREAM << "[map] w2v ppm=" << pixelsPerMeter << " halfW=" << halfWidth << " scale=" << currentScale << " m=[" << worldToView(0,0) << "," << worldToView(0,1) << "," << worldToView(0,2) << " | " << worldToView(1,0) << "," << worldToView(1,1) << "," << worldToView(1,2) << " | " << worldToView(2,0) << "," << worldToView(2,1) << "," << worldToView(2,2) << " | t " << worldToView(3,0) << "," << worldToView(3,1) << "," << worldToView(3,2) << "]\n" << std::flush; } BuildRadarShadow(worldToView); // FUN_004c228c flashState = !flashState; // this+0x374 ^= 1 localView.AttachRecorder(&previousDrawing); // (this+0x48 vtbl+0x64) const int mlog = getenv("BT_MAP_LOG") ? 1 : 0; if (mlog) DEBUG_STREAM << "[map] draw: wedge...\n" << std::flush; DrawViewWedge(); // FUN_004c2484 if (mlog) DEBUG_STREAM << "[map] draw: static...\n" << std::flush; DrawStatic(worldToView); // FUN_004c25b4 if (mlog) DEBUG_STREAM << "[map] draw: moving...\n" << std::flush; DrawMoving(worldToView); // FUN_004c2788 if (mlog) DEBUG_STREAM << "[map] draw: names...\n" << std::flush; DrawNames(worldToView); // FUN_004c28c4 if (mlog) DEBUG_STREAM << "[map] draw: done\n" << std::flush; localView.DetachRecorder(); // (this+0x48 vtbl+0x68) } // // @004c228c -- BuildRadarShadow. In the shipped build this compiled to an // empty body (the shadow-cast pass is disabled), so the allocated shadowTable // is never populated. Reconstructed as a no-op to match the binary. // (BEST-EFFORT: the intended algorithm -- ray-marching the shadowTable per // half-degree column -- is not present in the captured object.) // void MapDisplay::BuildRadarShadow(AffineMatrix & /*worldToView*/) { } // // @004c2484 -- draw the two edges of the player's field-of-view wedge as lines // from the map centre out to a far (clipped) radius. // void MapDisplay::DrawViewWedge() { Radian leftEdge = viewHorizontalRotation - viewHalfHorizontalWidth; Radian rightEdge = viewHorizontalRotation + viewHalfHorizontalWidth; localView.SetColor(staticColor); // (this+0x48 vtbl+0x18) Radian edges[2]; edges[0] = leftEdge; edges[1] = rightEdge; for (int i = 0; i < 2; ++i) { Scalar angle = -edges[i]; SinCosPair sc; sc = Radian(angle); // FUN_00408328 Scalar x = sc.sine * ViewWedgeRadius; // * 10000.0f Scalar z = sc.cosine * ViewWedgeRadius; localView.MoveToAbsolute(0, 0); // vtbl+0x24 (centre) localView.DrawLineToAbsolute(Round(x), Round(z)); // vtbl+0x30 } } // // @004c25b4 -- plot every in-range static entity as a blip, plus a name bitmap // for the special (labelled) ones. // void MapDisplay::DrawStatic(AffineMatrix &worldToView) { ChainIteratorOf iter(staticEntityList.GetInstanceList()); // FUN_0042ac1c(local_68, this+0x384) iter.First(); PipTable *pips = GetPipTable(renderer); // *(renderer+0x4C) for (;;) { Entity *entity = iter.ReadAndNext(); // vtbl+0x28 if (entity == NULL) { break; } Vector3D delta; delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644 if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z > maximumDistanceSquared) { continue; } // VISIBLE-RADIUS cull (port): the capability radius above (RadarPercent // x 4000 m) exceeds the SCOPE radius (currentScale/2 at the current // zoom); an out-of-scope draw lands beyond the port rect on the shared // gauge surface. Skip what the scope can't show. if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f) { continue; } Pip *pip = (pips != NULL) ? pips->LookUpPip(entity->GetResourceID()) // FUN_004688ce(pips+0x58,entity+0x1bc,0) : NULL; if (pip != NULL) { AffineMatrix blipXform; AffineMatrix entityXform; EntityTransform(&entityXform, entity); blipXform.Multiply(entityXform, worldToView); // FUN_0040b104(local_5c, entity+0xD0, ...) pip->Draw( // FUN_0046f0c0 LODIndex, metersPerPixel, &localView, staticColor, blipXform, 0); } // // Labelled static entities (e.g. nav beacons) also blit a name bitmap. // if (IsLabelledEntity(entity)) // FUN_0041a1a4 (LabelledEntityClass 0x4e70c4) { int nameID = GetNameID(entity); // entity+0x21C BitMap *bitmap = LookUpNameBitmap(nameID); // (cache+0x68 vtbl+0x0C) if (bitmap != NULL) { Point3D screen; screen.Multiply(EntityPosition(entity), worldToView); // FUN_00408b98 localView.MoveToAbsolute(Round(screen.x), Round(screen.z)); localView.SetColor(MapBlipColor()); // 7 localView.DrawBitMap(0, bitmap, 0, 0, bitmap->Data.Size.x - 1, bitmap->Data.Size.y - 1); } } } } // // @004c2788 -- plot every in-range moving entity (mechs, vehicles) as a blip, // skipping the operator's own mech and boxing the current target. // void MapDisplay::DrawMoving(AffineMatrix &worldToView) { ChainIteratorOf iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(local_1c, this+0x3A0) iter.First(); PipTable *pips = GetPipTable(renderer); // *(renderer+0x4C) Entity *owner = ResolveOperatorEntity(renderer); Entity *target = GetTarget(owner); // owner->GetVideoObject()->target const int mlog = getenv("BT_MAP_LOG") ? 1 : 0; if (mlog) DEBUG_STREAM << "[map] DrawMoving owner=" << (void*)owner << " view=(" << viewingPosition.x << "," << viewingPosition.z << ")\n" << std::flush; for (;;) { Entity *entity = iter.ReadAndNext(); if (entity == NULL) { break; } if (mlog) { Point3D ep = EntityPosition(entity); Scalar dx = ep.x - viewingPosition.x, dz = ep.z - viewingPosition.z; DEBUG_STREAM << "[map] ent=" << (void*)entity << " cls=0x" << std::hex << entity->GetClassID() << std::dec << " own=" << (entity == owner ? 1 : 0) << " pos=(" << ep.x << "," << ep.z << ")" << " dsq=" << (dx*dx + dz*dz) << "\n" << std::flush; } if (entity == owner) { continue; } Vector3D delta; delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644 if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z > maximumDistanceSquared) { continue; } // VISIBLE-RADIUS cull (port; see DrawStatic): don't draw past the scope. if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f) { continue; } Pip *pip = (pips != NULL) ? pips->LookUpPip(entity->GetResourceID()) : NULL; if (mlog) { Point3D sp; sp.Multiply(EntityPosition(entity), worldToView); DEBUG_STREAM << "[map] pip=" << (void*)pip << " screen=(" << sp.x << "," << sp.y << "," << sp.z << ")" << " lod=" << LODIndex << " mpp=" << metersPerPixel << "\n" << std::flush; } if (pip != NULL) { AffineMatrix blipXform; AffineMatrix entityXform; EntityTransform(&entityXform, entity); blipXform.Multiply(entityXform, worldToView); // FUN_0040b104 int boxColour = (entity == target) ? boxColor : 0; // highlight target pip->Draw( // FUN_0046f0c0 LODIndex, metersPerPixel, &localView, MapBlipColor() /*7*/, blipXform, boxColour); } else if (entity->GetClassID() == 0xBB9) // MechClassID -- radar contacts are mechs { // FALLBACK (kept, task #17): a mech whose model has no type-0x12 // pip resource (all shipped mechs HAVE one -- this covers dev // content) still draws the cross blip at the projected position. // // Project delta = (entity - viewpoint) directly to radar pixels: the // worldToView camera matrix bakes a scale that is wrong for a point // transform (|blip| came out ~3.8x |delta|*ppm), so compute it here -- // top-down, heading-up: rotate delta by -heading, then scale by // pixelsPerMeter. |screen| == |delta|*ppm, on-radar for in-range mechs. Scalar sinH = 0.0f, cosH = 1.0f; if (currentAngularPointer != NULL) { EulerAngles e; e = *currentAngularPointer; // Quaternion -> EulerAngles (operator=) SinCosPair scH; scH = Radian(e.yaw); // FUN_00408328 sinH = scH.sine; cosH = scH.cosine; } Scalar rx = delta.x * cosH + delta.z * sinH; Scalar rz = -delta.x * sinH + delta.z * cosH; int sx = Round(rx * pixelsPerMeter); int sz = Round(rz * pixelsPerMeter); if (getenv("BT_MAP_LOG")) DEBUG_STREAM << "[map] blip mech @(" << sx << "," << sz << ")" << " scale=" << currentScale << " ppm=" << pixelsPerMeter << " deltaXZ=(" << delta.x << "," << delta.z << ")\n" << std::flush; localView.SetColor((entity == target) ? boxColor : MapBlipColor()); localView.MoveToAbsolute(sx - 5, sz); localView.DrawLineToAbsolute(sx + 5, sz); localView.MoveToAbsolute(sx, sz - 5); localView.DrawLineToAbsolute(sx, sz + 5); // small box around the blip so it reads as a distinct contact marker localView.MoveToAbsolute(sx - 4, sz - 4); localView.DrawLineToAbsolute(sx + 4, sz - 4); localView.DrawLineToAbsolute(sx + 4, sz + 4); localView.DrawLineToAbsolute(sx - 4, sz + 4); localView.DrawLineToAbsolute(sx - 4, sz - 4); } } } // // @004c28c4 -- CONFIRMED (assert path "...BTL4RDR.CPP", line 0x408). // // Snapshot up to maximumNames moving entities into the name array, then draw // their labels. The hot-box colour flashes (flashState) on the target. // void MapDisplay::DrawNames(AffineMatrix &worldToView) { Entity *owner = ResolveOperatorEntity(renderer); Entity *target = GetTarget(owner); // *(*(owner+400)+0x284) ChainIteratorOf iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(local_1c, this+0x3A0) iter.First(); int count = iter.GetSize(); // vtbl+0x14 if (getenv("BT_MAP_LOG")) DEBUG_STREAM << "[map] names count=" << count << "\n" << std::flush; if (count < 1) { return; } if (count > maximumNames) { count = maximumNames; } // // First pass: extract. // for (int i = 0; i < count; ++i) { Entity *entity = iter.ReadAndNext(); // vtbl+0x28 if (entity == NULL) { Verify(False); // FUN_0040385c // "MapDisplay::DrawNames -- GetSize() / iterator mismatch" // "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x408 } else { // VISIBLE-RADIUS cull (port; see DrawStatic): a label for a // contact beyond the scope would blit outside the port rect. Vector3D d; d.Subtract(EntityPosition(entity), viewingPosition); if (d.x*d.x + d.z*d.z > currentScale * currentScale * 0.25f) entity = NULL; // extract guards NULL -> no label } nameArray[i].ExtractFromEntity(entity, worldToView, owner, target); // FUN_004c19fc } // // Second pass: draw. The target's label flashes in boxColor. // int hotboxColor = flashState ? boxColor : 0; for (int i = 0; i < count; ++i) { nameArray[i].Draw(&localView, hotboxColor); // FUN_004c1ab0 } } //########################################################################### //########################################################################### // BT radar adaptation layer -- implementations // // These reproduce the BT-specific facilities the recovered radar relied on. // The WinTesla engine we compile against does not expose them, so they are // provided here, best-effort, resolving to "unavailable" until wired to the // real BattleTech gauge renderer / video object system (CROSS-FAMILY NEEDS). //########################################################################### //########################################################################### // (task #17: the Pip::Draw / PipTable::LookUpPip stub bodies that lived here // are retired -- both are inline facades over the engine's L4GaugeImage / // L4Warehouse::gaugeImageBin now, see the class declarations above.) // // FUN_00417ab4 -- the gauge renderer's "operator entity" (the mech whose // cockpit this gauge belongs to). The renderer's linked-entity socket may be // unwired on WinTesla, so fall back to the application viewpoint entity (the mech // we are driving) -- the same fallback HeadingPointer::Execute uses. Returning // NULL crashes the map's CalculateBounds/DrawDisplay (EntityPosition(NULL)). // static Entity * ResolveOperatorEntity(GaugeRenderer *renderer) { Entity *e = renderer ? ((L4GaugeRenderer *)renderer)->GetLinkedEntity() : NULL; if (e == NULL && application != NULL) e = (Entity *)application->GetViewpointEntity(); return e; } // // renderer+0x4C -- GaugeRenderer::warehousePointer: the L4Warehouse whose // gaugeImageBin (+0x58) is the pip table (task #17; the "not present on // WinTesla" note was wrong -- the facility is engine T0, L4WRHOUS.h). // static PipTable * GetPipTable(GaugeRenderer *renderer) { return (renderer != NULL) ? (PipTable *)renderer->warehousePointer : NULL; } // // The "global name-bitmap cache" DECODED (task #17): DAT_004efc94+0xC8 is // Application::currentMission, and the cache is the Mission's SMALL // name-bitmap VChain (prebuilt 64x16 egg rasters keyed 1-based by the egg // 'bitmapindex'). All T0 (MISSION.h / APP.h). // static BitMap * LookUpNameBitmap(int name_ID) { Mission *mission = (application != NULL) ? application->GetCurrentMission() : NULL; if (mission == NULL || name_ID <= 0) { return NULL; } return mission->GetSmallNameBitmap(name_ID); } // // entity+0x21C -- Landmark::landmarkID (cultural.h; the static loop's label // key into the LARGE chain). DORMANT in shipped content: no runtime writer // exists (the id was baked by the 1995 map tool) and no [landmarks] section // exists in any egg -- kept faithful-but-inert [T1]. // static int GetNameID(Entity * /*entity*/) { return -1; } // // FUN_0041a1a4 vs 0x4e70c4 -- IsDerivedFrom("Landmark") (task #17 decode: // the "LabelledEntityClass" IS the Landmark class, parent CulturalIcon). // Dormant with GetNameID above (no landmark content ships). // static Logical IsLabelledEntity(Entity * /*entity*/) { return False; } // // owner->GetVideoObject()->target. // static Entity * GetTarget(Entity *owner) { // *(*(owner+0x190)+0x284) -- the owning BTPlayer's objectiveMech (the // designated target; complete-type bridge, btplayer.cpp). extern Entity *BTPlayerObjectiveMechOf(void *player); Player *owning_player = (owner != NULL) ? owner->GetOwningPlayer() : NULL; return (owning_player != NULL) ? BTPlayerObjectiveMechOf(owning_player) : NULL; } // // FUN_0041f98c -- Entity::FindSubObject(name) (locates "Torso"). // static Entity * FindSubObject(Entity * /*entity*/, const char * /*name*/) { return NULL; } // // torso+0x1D8 -- the sub-object's horizontal (yaw) rotation feed. // static Radian * GetHorizontalRotation(Entity * /*entity*/) { return NULL; } //########################################################################### //########################################################################### // Local data-source connection helpers (file-private to btl4rdr.cpp) // // The MapDisplay constructor wires "direct-of-pointer" gauge connections so // that an externally-owned Point3D*, Quaternion* or Radian is copied into the // corresponding MapDisplay member every frame. In the surviving engine these // are the GaugeConnectionDirectOf template (GAUGE.HPP), so the explicit // per-type subclasses the decompiler emitted (vtables 00514220 / 00514214 / // 00514208) are just template instantiations -- no separate code needed. //########################################################################### //###########################################################################