//===========================================================================// // 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 (source410 re-host) from the shipped binary (BTL4OPT.EXE) // against the SURVIVING header CODE\BT\BT_L4\BTL4RDR.HPP, whose member byte- // offsets match the decompiled object exactly: // // nameArray[16] @0x090 backgroundColor @0x358 // offsetPosition @0x2D0 staticColor @0x35C // currentScale @0x2D4 boxColor @0x360 // currentPositionPointer @0x2D8 halfWidth @0x364 // currentAngularPointer @0x2DC halfHeight @0x368 // currentCapabilitiesRatio@0x2E0 operating @0x36C // LODIndex @0x2E4 rockAndRoll @0x370 // pixelsPerMeter @0x2E8 flashState @0x374 // metersPerPixel @0x2EC staticEntityList @0x378 // maximumRange @0x2F0 movingEntityList @0x394 // maximumDistanceSquared @0x2F4 previousDrawing @0x3B0 // xMin..zMax @0x2F8.. halfViewWidthInUnits@0x3C0 // viewingPosition @0x310 shadowTable @0x3C4 // viewHorizontalRotation @0x31C worldToView @0x324 // viewHalfHorizontalWidth @0x320 // // The TU is CONFIRMED by the embedded assert path "d:\tesla\bt\bt_l4\ // BTL4RDR.CPP" (CalculateBounds line 0x224, DrawNames line 0x408). // // RE-HOST DELTAS vs the WinTesla-port donor (see GAUGE-BLOCK.NOTES.md, // btl4rdr section): // * the radar entity lists are fed by the AUTHENTIC interesting-entity // notifications (BTL4GaugeRenderer::NotifyOfNewInterestingEntity -> // GaugeRenderer movingEntities/staticEntities) -- the port-only // RebuildEntityGrid call and the three visible-radius cull blocks are // retired, as is the port's fallback mech cross-blip branch; // * KEPT (correctness-load-bearing): the rotation-only-assignment-then- // translation-row-LAST compose, the true affine Invert, and the // YawPitchRoll (yaw-first) heading decode in DrawDisplay; // * the torso-twist view-wedge feed reads Mech::CurrentTorsoTwist() through // a file-private connection (the reconstructed Torso::currentTwist is // protected -- no raw 1995 byte offsets); // * the "pip" symbol system is the engine's L4GaugeImage, cached in // L4Warehouse::gaugeImageBin (resource type 0x12), used directly. // #include #pragma hdrstop #if !defined(BTL4RDR_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(MISSION_HPP) # include #endif #if !defined(PLAYER_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(L4WRHOUS_HPP) # include #endif // // 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; } //########################################################################### //########################################################################### // File-local helpers //########################################################################### //########################################################################### // // Entity placement helpers -- the entity keeps its world placement in // localOrigin (linearPosition @+0x100 + angularPosition). These give the // radar the Point3D position and AffineMatrix transform the draw loops use. // static inline Point3D & EntityPosition(Entity *entity) { return entity->localOrigin.linearPosition; } static inline void EntityTransform(AffineMatrix *out, Entity *entity) { *out = entity->localOrigin; // entity+0xD0 } // // FUN_00417ab4 -- the gauge renderer's "operator entity" (the mech whose // cockpit this gauge belongs to): the renderer's linked-entity socket // (renderer+0x40), armed by the viewpoint-bind broadcast before the per-model // gauges are configured (BTL4APP.CPP:451 ConfigureForModel). Renderer:: // GetLinkedEntity has been public "for gauges" since 05/25/95. // static Entity * ResolveOperatorEntity(GaugeRenderer *renderer) { return (renderer != NULL) ? renderer->GetLinkedEntity() : NULL; } // // renderer+0x4C -- GaugeRenderer::warehousePointer: the L4Warehouse whose // gaugeImageBin (+0x58) holds the radar "pip" vector shapes (L4GaugeImage, // resource type 0x12; BTL4.RES ships 110 of them -- every mech / vehicle / // building / tree). // static L4Warehouse * GetGaugeWarehouse(GaugeRenderer *renderer) { return (renderer != NULL) ? (L4Warehouse *)renderer->warehousePointer : NULL; } // // FUN_004688ce(warehouse+0x58, entity+0x1BC, 0) -- the refcounting bin Get, // exactly as the binary's per-frame draw loops call it (one vector shape per // model; the mission-end warehouse Purge reclaims the references). // static L4GaugeImage * LookUpGaugeImage( L4Warehouse *warehouse, ResourceDescription::ResourceID resource_ID ) { return warehouse->gaugeImageBin.Get(resource_ID, False); } // // The "global name-bitmap cache": the current Mission's SMALL name-bitmap // chain (prebuilt 64x16 egg rasters keyed 1-based by the egg 'bitmapindex'). // 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 (the static loop's label key into the // LARGE name 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 (no landmark ever labels). // static int GetNameID(Entity * /*entity*/) { return -1; } // // FUN_0041a1a4 vs 0x4e70c4 -- IsDerivedFrom(Landmark). Dormant with // GetNameID above (no landmark content ships) -- INERT. // static Logical IsLabelledEntity(Entity * /*entity*/) { return False; } // // The operator's current target -- the owning BTPlayer's objectiveMech // (binary: *(*(owner+0x190)+0x284)). INERT FEED: BTPlayer::objectiveMech // exists in the reconstruction (btplayer.hpp) but is protected with no // accessor reconstructed AND no writer yet (btplayer.cpp only NULLs it in the // constructor), so the target always resolves NULL here -- blips and labels // draw normally, the hot-box / flash highlight simply never engages. Wire // through a BTPlayer accessor once the targeting wave reconstructs the // objectiveMech writer. // static Entity * GetTarget(Entity * /*owner*/) { return NULL; } //########################################################################### //########################################################################### // MapTwistConnection (file-private) // // The binary wired a direct-of-Radian gauge connection onto the mech's // "Torso" sub-object horizontal rotation (Torso::currentTwist, torso+0x1D8; // roster walk FUN_0041f98c) so the view wedge follows the torso twist. // RE-HOST (databinding rule): currentTwist is protected on the reconstructed // Torso, so this connection reads the documented accessor // Mech::CurrentTorsoTwist() (mech.hpp -- NULL-safe: 0 with no torso // subsystem, matching the binary's no-torso -> heading-0 behaviour) and // copies it into MapDisplay::viewHorizontalRotation every frame, per the // engine GaugeConnection contract (GAUGE.HPP:151). //########################################################################### //########################################################################### class MapTwistConnection: public GaugeConnection { public: MapTwistConnection( int parameter_ID, Radian *data_destination, Mech *twist_source ); protected: void Update(); Mech *twistSource; Radian *dataDestination; }; MapTwistConnection::MapTwistConnection( int parameter_ID, Radian *data_destination, Mech *twist_source ): GaugeConnection(parameter_ID) { Check_Pointer(this); Check_Pointer(data_destination); Check_Pointer(twist_source); dataDestination = data_destination; twistSource = twist_source; *dataDestination = twistSource->CurrentTorsoTwist(); Check(this); } void MapTwistConnection::Update() { Check(this); Check_Pointer(dataDestination); Check_Pointer(twistSource); *dataDestination = twistSource->CurrentTorsoTwist(); } //########################################################################### //########################################################################### // 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 mission's 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 the mission's // name-bitmap loader). // 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 = (Logical)(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)); // vtbl+0x24 if (!hotBoxed) { graphics_view->SetColor(color); // vtbl+0x18 } else { graphics_view->SetColor(hotbox_color); } graphics_view->DrawBitMap( // vtbl+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. // 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 *parameterList = methodDescription.parameterList; # if DEBUG_LEVEL > 0 parameterList[0].CheckIt(ParameterDescription::typeRate); // rate ID parameterList[1].CheckIt(ParameterDescription::typeModeMask); // mode mask parameterList[2].CheckIt(ParameterDescription::typeVector); // size parameterList[3].CheckIt(ParameterDescription::typeString); // offset pos parameterList[4].CheckIt(ParameterDescription::typeInteger); // view width parameterList[5].CheckIt(ParameterDescription::typeColor); // background parameterList[6].CheckIt(ParameterDescription::typeColor); // static parameterList[7].CheckIt(ParameterDescription::typeColor); // hotbox parameterList[8].CheckIt(ParameterDescription::typeScalar); // max range parameterList[9].CheckIt(ParameterDescription::typeAttribute); // scale parameterList[10].CheckIt(ParameterDescription::typeAttribute); // position parameterList[11].CheckIt(ParameterDescription::typeAttribute); // angle parameterList[12].CheckIt(ParameterDescription::typeAttribute); // capabilities # endif Check(gauge_renderer); OffsetPosition offset = (strcmp(parameterList[3].data.string, "bottom") == 0) ? bottom : center; # if DEBUG_LEVEL > 0 MapDisplay *map_display = # endif new MapDisplay( parameterList[0].data.rate, parameterList[1].data.modeMask, (L4GaugeRenderer *) gauge_renderer, 0, // owner_ID display_port_index, position.x, position.y, // left, bottom position.x + parameterList[2].data.vector.x, // right position.y + parameterList[2].data.vector.y, // top offset, parameterList[4].data.integer, // view width (degrees) parameterList[5].data.color, // background colour parameterList[6].data.color, // static colour parameterList[7].data.color, // hotbox colour parameterList[8].data.scalar, // maximum range entity, (Scalar *) parameterList[9].data.attributePointer, // scale (RadarRange) (Point3D **) parameterList[10].data.attributePointer, // position (RadarLinearPosition) (Quaternion **) parameterList[11].data.attributePointer, // angle (RadarAngularPosition) (Scalar *) parameterList[12].data.attributePointer // capabilities (Avionics/RadarPercent) // HACK!!!! The attribute casts are VERY dangerous! (The native // idiom -- see NumericDisplayScalar::Make, L4GAUGE.CPP.) ); Register_Object(map_display); return True; } //############################################################################# // Construction / Destruction // // @004c1c24 -- vtable 0051422c, base name "MapDisplay". nameArray, the two // entity lists and previousDrawing are member objects (the decompiler's // explicit Construct_Array / ctor thunks are the compiler-generated member // construction). 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-twist 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" ) { // // Size the graphics port and centre the origin. // localView.SetPositionWithinPort(left, bottom, right, top); // 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; // @0x358 staticColor = static_color; // @0x35C boxColor = hotbox_color; // @0x360 maximumRange = maximum_range; // @0x2F0 rockAndRoll = allow_rock_and_roll; // @0x370 halfWidth = (right - left) >> 1; // @0x364 halfHeight = (top - bottom) >> 1; // @0x368 offsetPosition = offset_position; // @0x2D0 // // The radar shadow table: the binary allocates halfViewWidthInUnits*8 // bytes = one ShadowRecord per half-degree column across the full view // width. Never populated in the shipped build (BuildRadarShadow below // compiled empty) -- allocated for fidelity. // halfViewWidthInUnits = view_width_in_degrees >> 1; // @0x3C0 shadowTable = new ShadowRecord[halfViewWidthInUnits * 2]; // @0x3C4 viewHorizontalRotation = 0.0f; // @0x31C viewHalfHorizontalWidth = 0.3926991f; // @0x320 = PI/8 flashState = False; // @0x374 // // 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 (MechClassID 0xBB9), feed its live torso // twist into the map's view heading so the view wedge follows the torso. // Binary: "Torso" roster walk (FUN_0041f98c) + direct-of-Radian // connection onto torso+0x1D8 (FUN_004c2d42); re-host: the accessor-based // MapTwistConnection above (see its comment banner). // if (entity->GetClassID() == (Entity::ClassID)MechClassID) { AddConnection( new MapTwistConnection( 0, &viewHorizontalRotation, Cast_Object(Mech *, entity))); } } // // @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. // MapDisplay::~MapDisplay() { 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) { cout << indent << "ThreatIndicator:"; // FUN_004dbb24 x2 char deeper[80]; strcpy(deeper, indent); strcat(deeper, "\t"); // FUN_004d49b8 GraphicGauge::ShowInstance(deeper); // FUN_004448bc } // // @004c1fc0 -- reset the per-frame phase counter when the gauge is shown. // void MapDisplay::BecameActive() { operatingPhase = 0; // @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 // // The renderer's movingEntities / staticEntities records queried in phases // 1/2 are populated by the AUTHENTIC interesting-entity notifications // (BTL4GaugeRenderer::NotifyOfNewInterestingEntity, btl4grnd.cpp -- Mover -> // movingEntities, Terrain -> staticEntities, gated on the entity owning a // type-0x12 gauge-image resource). Note the authentic grid quirks the range // cull below compensates for: GaugeEntityList::CopyEntitiesWithinBounds's // per-entity box test is commented out in GAUGMAP.CPP (grid-cell granular // only), so this gauge's maximumDistanceSquared test in DrawStatic/DrawMoving // is the REAL range filter. // void MapDisplay::Execute() { int phase = operatingPhase++; // @0x354 if (phase == 0) { operating = (Logical)(OperatingEpsilon < currentCapabilitiesRatio); if (operating) { maximumDistanceSquared = currentCapabilitiesRatio * maximumRange; maximumDistanceSquared = maximumDistanceSquared * maximumDistanceSquared; CalculateBounds(); // FUN_004c2178 } } else if (phase == 1) { staticEntityList.Clear(); // FUN_004435ac(this+0x378) if (operating) { renderer->GetStaticEntitiesWithinBounds( // staticEntities @+0xB0 &staticEntityList, xMin, yMin, zMin, xMax, yMax, zMax); } } else if (phase == 2) { movingEntityList.Clear(); // FUN_004435ac(this+0x394) if (operating) { renderer->GetMovingEntitiesWithinBounds( // movingEntities @+0x94 &movingEntityList, xMin, yMin, zMin, xMax, yMax, zMax); } } else { if (phase == 3) { EraseDisplay(); // FUN_004c2294 if (operating) { DrawDisplay(); // FUN_004c22c4 } // // Re-host verification trace (worksheet open question: does the // authentic NotifyOf*InterestingEntity feed populate the radar // lists once RebuildEntityGrid is retired?). Logs only when the // gathered counts change. // if (getenv("BT_MECH_LOG") != NULL) { static int previousStaticCount = -1; static int previousMovingCount = -1; int static_count = staticEntityList.NumberOfItems(); int moving_count = movingEntityList.NumberOfItems(); if (static_count != previousStaticCount || moving_count != previousMovingCount) { previousStaticCount = static_count; previousMovingCount = moving_count; DEBUG_STREAM << "MapDisplay: operating=" << (int)operating << " static=" << static_count << " moving=" << moving_count << endl << flush; } } } operatingPhase = 0; } } // // @004c2178 -- 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) { // // "MapDisplay::CalculateBounds detected a bad scale" // ("d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x224) // Verify(False); // FUN_0040385c currentScale = 1.0f; // 0x3f800000 } pixelsPerMeter = (Scalar)(halfWidth * 2) / currentScale; metersPerPixel = MetersPerPixelNum / pixelsPerMeter; // 1.0 / ppm LODIndex = metersPerPixel; Scalar halfWorldWidth = (Scalar)halfWidth / pixelsPerMeter; Scalar halfWorldHeight = (Scalar)halfHeight / pixelsPerMeter; Entity *owner = ResolveOperatorEntity(renderer); // FUN_00417ab4(renderer+0x40) Check_Pointer(owner); viewingPosition = EntityPosition(owner); // FUN_00408440(this+0x310) 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 (the decomp shows the view.BuildIdentity(); // identity built twice -- faithful) // // 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 // // TRANSCRIPTION FIX (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, centre PRESERVED. A full // compose (view *= yaw) would also rotate the translation // (view = [R | center x R]) -- the subsequent Invert then maps // center x R to the origin instead of the viewer, so every blip lands // 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. // // FIX (kept on re-host): YawPitchRoll (yaw-first), not // EulerAngles -- the EulerAngles(Quaternion) decomposition is // ambiguous for a yawing mech: as yaw sweeps past +/-pi the // quaternion double-cover flips it onto a pitch=roll=pi // branch, so euler.yaw REVERSES (the radar spins back). // YawPitchRoll applies yaw FIRST -> a clean continuous 360 // degree heading (ROTATION.HPP:162, operator= at :190). // YawPitchRoll euler; euler = *currentAngularPointer; Scalar heading = (Scalar)euler.yaw * HeadingHalf; // * 0.5f half-angle 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) } else { // // "Rock and roll": use the full orientation. // view = *currentAngularPointer; // rotation-only build } } view.SetFromAxis(W_Axis, center); // FUN_0040b0ac -- translation LAST } // // TRANSCRIPTION FIX (the invisible-pip bug): FUN_0040b244 is the full // affine INVERSE (cofactor expansion + determinant divide), not a copy. // `view` above is the OPERATOR's local->world pose (translate to the mech // + yaw); the radar needs its inverse -- world->scope, centre subtracted, // heading unrotated. Read as a copy, every pip/name transforms to // (world + centre) x ppm = thousands of pixels off-scope: the scope draws // empty at all times. // worldToView.Invert(view); // FUN_0040b244(this+0x324) // // Bake the metres->pixels scale into the matrix. // Vector3D scale; scale.x = scale.y = scale.z = pixelsPerMeter; worldToView.Multiply(worldToView, scale); // FUN_0040b374 BuildRadarShadow(worldToView); // FUN_004c228c flashState = (Logical)!flashState; // @0x374 ^= 1 localView.AttachRecorder(&previousDrawing); // vtbl+0x64 DrawViewWedge(); // FUN_004c2484 DrawStatic(worldToView); // FUN_004c25b4 DrawMoving(worldToView); // FUN_004c2788 DrawNames(worldToView); // FUN_004c28c4 localView.DetachRecorder(); // 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() { int i; Radian leftEdge = viewHorizontalRotation - viewHalfHorizontalWidth; Radian rightEdge = viewHorizontalRotation + viewHalfHorizontalWidth; localView.SetColor(staticColor); // vtbl+0x18 Radian edges[2]; edges[0] = leftEdge; edges[1] = rightEdge; for (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(this+0x384) iter.First(); L4Warehouse *pips = GetGaugeWarehouse(renderer); // *(renderer+0x4C) for (;;) { Entity *entity = iter.ReadAndNext(); // vtbl+0x28 if (entity == NULL) { break; } // // The REAL range filter (the grid query is cell-granular only -- // see the Execute banner). // Vector3D delta; delta.Subtract(EntityPosition(entity), viewingPosition); // FUN_00408644 if (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z > maximumDistanceSquared) { continue; } L4GaugeImage *pip = (pips != NULL) ? LookUpGaugeImage(pips, entity->GetResourceID()) // FUN_004688ce : NULL; if (pip != NULL) { AffineMatrix blipXform; AffineMatrix entityXform; EntityTransform(&entityXform, entity); blipXform.Multiply(entityXform, worldToView); // FUN_0040b104 pip->Draw( // FUN_0046f0c0 LODIndex, metersPerPixel, &localView, staticColor, blipXform, 0); } // // Labelled static entities (e.g. nav beacons) also blit a name // bitmap. (Dormant: no landmark content ships -- see the inert // helpers above.) // if (IsLabelledEntity(entity)) // FUN_0041a1a4 (Landmark 0x4e70c4) { int nameID = GetNameID(entity); // entity+0x21C BitMap *bitmap = LookUpNameBitmap(nameID); 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(this+0x3A0) iter.First(); L4Warehouse *pips = GetGaugeWarehouse(renderer); // *(renderer+0x4C) Entity *owner = ResolveOperatorEntity(renderer); Entity *target = GetTarget(owner); // objectiveMech (inert feed) for (;;) { Entity *entity = iter.ReadAndNext(); if (entity == NULL) { break; } 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; } L4GaugeImage *pip = (pips != NULL) ? LookUpGaugeImage(pips, entity->GetResourceID()) : NULL; 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); } } } // // @004c28c4 -- CONFIRMED by the embedded assert path // "d:\tesla\bt\bt_l4\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) { int i; Entity *owner = ResolveOperatorEntity(renderer); Entity *target = GetTarget(owner); // objectiveMech (inert feed) ChainIteratorOf iter(movingEntityList.GetInstanceList()); // FUN_0042ac1c(this+0x3A0) iter.First(); int count = iter.GetSize(); // vtbl+0x14 if (count < 1) { return; } if (count > maximumNames) { count = maximumNames; } // // First pass: extract. // for (i = 0; i < count; ++i) { Entity *entity = iter.ReadAndNext(); // vtbl+0x28 if (entity == NULL) { // // "MapDisplay::DrawNames -- GetSize() / iterator mismatch" // ("d:\tesla\bt\bt_l4\BTL4RDR.CPP", line 0x408) // Verify(False); // FUN_0040385c } 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 (i = 0; i < count; ++i) { nameArray[i].Draw(&localView, hotboxColor); // FUN_004c1ab0 } }