phase(N.1): delete legacy scenery code path; WB is the only path
Phase N.1 step 8 (final code cleanup): now that ACDREAM_USE_WB_SCENERY
has been default-on (commit b84ecbd), remove the legacy in-line
algorithms so we don't accumulate dead-code drift.
Deleted:
- SceneryGenerator.UseWbScenery (feature flag)
- SceneryGenerator.IsOnRoad / DisplaceObject / RoadHalfWidth (legacy
ports — Generate used to call them)
- The legacy in-line implementation in Generate()
- SceneryGeneratorTests.DisplaceObject_* (test the deleted method)
- SceneryWbConformanceTests.cs entirely (purpose served — proved
equivalence pre-migration; would compare WB to WB after delete)
Renamed:
- GenerateViaWb -> GenerateInternal (it's the only path now)
Kept:
- Public IsRoadVertex predicate (small surface, useful)
- WbSceneryAdapter (consumed by GenerateInternal)
- All WbSceneryAdapterTests (still cover the adapter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b84ecbda51
commit
b0ec6deb50
3 changed files with 24 additions and 615 deletions
|
|
@ -25,11 +25,11 @@ namespace AcDream.Core.World;
|
|||
/// (scale hash constant 0x7f51=32593 not in dumped chunks;
|
||||
/// confirmed against ACViewer which matches all other constants)
|
||||
///
|
||||
/// Key implementation note: the decompiled client computes each LCG value as a
|
||||
/// signed 32-bit int, then normalises with "if (val < 0) val += 2^32" before
|
||||
/// dividing by 2^32. This is equivalent to our unchecked((uint)(...)) cast.
|
||||
/// ACViewer's reference omits this cast and is subtly wrong for negative inputs.
|
||||
/// We deliberately match the decompiled client, not ACViewer.
|
||||
/// Phase N.1 (2026-05-08): migrated all algorithm calls to WorldBuilder's
|
||||
/// <c>SceneryHelpers</c> + <c>TerrainUtils</c>. The legacy in-line implementations
|
||||
/// have been removed; <c>WbSceneryAdapter</c> bridges <c>LandBlock</c> data to WB's
|
||||
/// <c>TerrainEntry[]</c>. See
|
||||
/// <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
|
||||
/// </summary>
|
||||
public static class SceneryGenerator
|
||||
{
|
||||
|
|
@ -37,15 +37,7 @@ public static class SceneryGenerator
|
|||
private const int VerticesPerSide = 9;
|
||||
private const float CellSize = 24.0f;
|
||||
private const float LandblockSize = 192.0f; // 8 cells * 24 units
|
||||
|
||||
/// <summary>
|
||||
/// Phase N.1: scenery placement uses WorldBuilder's <c>SceneryHelpers</c>
|
||||
/// + <c>TerrainUtils</c> by default. Set <c>ACDREAM_USE_WB_SCENERY=0</c>
|
||||
/// to restore the legacy in-line algorithms (escape hatch — to be deleted
|
||||
/// in Task 8 once we have a session of green visuals).
|
||||
/// </summary>
|
||||
internal static readonly bool UseWbScenery =
|
||||
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_SCENERY") != "0";
|
||||
private const int CellsPerSide = 8;
|
||||
|
||||
public readonly record struct ScenerySpawn(
|
||||
uint ObjectId, // GfxObj or Setup id
|
||||
|
|
@ -54,12 +46,9 @@ public static class SceneryGenerator
|
|||
float Scale);
|
||||
|
||||
/// <summary>
|
||||
/// Generate all scenery entries for one landblock. Uses the bit-packed
|
||||
/// TerrainInfo Type (bits 2-6) and Scenery (bits 11-15) fields to index into
|
||||
/// Region.TerrainInfo.TerrainTypes[type].SceneTypes[scenery] → a SceneInfo
|
||||
/// index into Region.SceneInfo.SceneTypes[sceneInfo].Scenes. Each cell picks
|
||||
/// one scene via a pseudo-random hash of the cell's global coordinates, then
|
||||
/// iterates the scene's ObjectDesc entries with per-object frequency rolls.
|
||||
/// Generate all scenery entries for one landblock. Phase N.1 migrated this
|
||||
/// to call WorldBuilder's <c>SceneryHelpers</c> + <c>TerrainUtils</c>;
|
||||
/// see <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ScenerySpawn> Generate(
|
||||
DatCollection dats,
|
||||
|
|
@ -69,210 +58,20 @@ public static class SceneryGenerator
|
|||
HashSet<int>? buildingCells = null,
|
||||
float[]? heightTable = null)
|
||||
{
|
||||
// Phase N.1: route to the WorldBuilder-backed implementation when
|
||||
// ACDREAM_USE_WB_SCENERY=1. See
|
||||
// docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md.
|
||||
if (UseWbScenery)
|
||||
return GenerateViaWb(dats, region, block, landblockId, buildingCells);
|
||||
|
||||
var result = new List<ScenerySpawn>();
|
||||
|
||||
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
||||
return result;
|
||||
|
||||
uint blockX = (landblockId >> 24) * 8; // 8 cells per landblock
|
||||
uint blockY = ((landblockId >> 16) & 0xFFu) * 8;
|
||||
|
||||
// RETAIL iterates 9×9 = 81 VERTICES, not 8×8 = 64 cells.
|
||||
// Named retail: CLandBlock::get_land_scenes (0x00530460) uses
|
||||
// `side_vertex_count` (offset 0x40, value 9) as the loop bound.
|
||||
// The do-while condition `(var+1) < side_vertex_count` runs var 0..8.
|
||||
// Edge vertices (x=8 or y=8) produce valid spawns when the per-object
|
||||
// displacement shifts the position back into the [0, 192) range.
|
||||
for (int x = 0; x < VerticesPerSide; x++)
|
||||
{
|
||||
for (int y = 0; y < VerticesPerSide; y++)
|
||||
{
|
||||
int i = x * VerticesPerSide + y;
|
||||
ushort raw = block.Terrain[i];
|
||||
|
||||
uint terrainType = (uint)((raw >> 2) & 0x1F); // bits 2-6
|
||||
uint sceneType = (uint)((raw >> 11) & 0x1F); // bits 11-15
|
||||
|
||||
// NOTE: retail does NOT skip based on this vertex's road bit.
|
||||
// The road test happens AFTER displacement via the 4-corner
|
||||
// polygonal OnRoad check (see below). Removing the
|
||||
// pre-displacement early-exit restores retail behavior.
|
||||
|
||||
if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue;
|
||||
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
||||
if (sceneType >= sceneTypeList.Count) continue;
|
||||
|
||||
uint sceneInfo = sceneTypeList[(int)sceneType];
|
||||
if (sceneInfo >= region.SceneInfo.SceneTypes.Count) continue;
|
||||
|
||||
var scenes = region.SceneInfo.SceneTypes[(int)sceneInfo].Scenes;
|
||||
if (scenes.Count == 0) continue;
|
||||
|
||||
uint cellX = (uint)x;
|
||||
uint cellY = (uint)y;
|
||||
uint globalCellX = cellX + blockX;
|
||||
uint globalCellY = cellY + blockY;
|
||||
|
||||
// Scene-selection hash: picks one scene from the terrain's scene list.
|
||||
// Decompiled: chunk_00530000.c line 1144
|
||||
// iVar5 = (iVar8 * 0x2a7f2b89 + 0x6c1ac587) * iVar9 + iVar8 * -0x421be3bd + 0x7f8cda01
|
||||
// where iVar8=globalCellX, iVar9=globalCellY.
|
||||
uint cellMat = globalCellY * (712977289u * globalCellX + 1813693831u)
|
||||
- 1109124029u * globalCellX + 2139937281u;
|
||||
double offset = cellMat * 2.3283064e-10;
|
||||
int sceneIdx = (int)(scenes.Count * offset);
|
||||
if (sceneIdx >= scenes.Count || sceneIdx < 0) sceneIdx = 0;
|
||||
|
||||
uint sceneId = (uint)scenes[sceneIdx];
|
||||
var scene = dats.Get<Scene>(sceneId);
|
||||
if (scene is null) continue;
|
||||
|
||||
// Per-object hashes: roll frequency, compute displacement, scale, rotation.
|
||||
// Decompiled: chunk_00530000.c lines 1168-1174
|
||||
// iStack_60 = iVar9 * 0x6c1ac587 → cellYMat
|
||||
// uStack_78 = iVar9 * iVar8 * 0x5111bfef + 0x70892fb7 → cellMat2
|
||||
// iStack_64 = iVar8 * -0x421be3bd → cellXMat
|
||||
// initial: local_90 = uStack_78 * 0x5b67 (j=0 term)
|
||||
// per-loop: iStack_70 = (iStack_60 - local_90) + iStack_64; local_90 += uStack_78
|
||||
// ⟹ iStack_70 = cellYMat - cellMat2 * (0x5b67 + j) + cellXMat
|
||||
uint cellXMat = unchecked(0u - 1109124029u * globalCellX);
|
||||
uint cellYMat = 1813693831u * globalCellY;
|
||||
uint cellMat2 = 1360117743u * globalCellX * globalCellY + 1888038839u;
|
||||
|
||||
for (uint j = 0; j < scene.Objects.Count; j++)
|
||||
{
|
||||
var obj = scene.Objects[(int)j];
|
||||
if (obj.WeenieObj != 0) continue; // Weenie entries are dynamic spawns, not static scenery
|
||||
|
||||
// Frequency roll: chunk_00530000.c line 1174 + 1179
|
||||
// (fVar1 * _DAT_007c6f10 < (float)piVar11[0x11]) → noise < obj.Frequency
|
||||
double noise = unchecked((uint)(cellXMat + cellYMat - cellMat2 * (23399u + j))) * 2.3283064e-10;
|
||||
if (noise >= obj.Frequency) continue;
|
||||
|
||||
// Displacement: pseudo-random offset within the cell.
|
||||
var localPos = DisplaceObject(obj, globalCellX, globalCellY, j);
|
||||
|
||||
float lx = cellX * CellSize + localPos.X;
|
||||
float ly = cellY * CellSize + localPos.Y;
|
||||
|
||||
if (lx < 0 || ly < 0 || lx >= LandblockSize || ly >= LandblockSize)
|
||||
continue;
|
||||
|
||||
// Retail post-displacement road check (FUN_00530d30).
|
||||
// Ported from ACViewer Landblock.OnRoad — uses the 4-corner
|
||||
// road bits of the containing cell plus the 5-unit road
|
||||
// half-width to test whether the displaced (lx,ly) lies on
|
||||
// the road ribbon.
|
||||
bool isOnRoad = IsOnRoad(block, lx, ly);
|
||||
if (isOnRoad)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Per-spawn building check on the DISPLACED position's cell.
|
||||
// Retail: CSortCell::has_building(cell) per spawn, not per vertex.
|
||||
// WorldBuilder: buildingsGrid[gx2, gy2] with 8×8 cell grid.
|
||||
if (buildingCells is not null)
|
||||
{
|
||||
int dcx = Math.Clamp((int)(lx / CellSize), 0, CellsPerSide - 1);
|
||||
int dcy = Math.Clamp((int)(ly / CellSize), 0, CellsPerSide - 1);
|
||||
if (buildingCells.Contains(dcx * VerticesPerSide + dcy))
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slope filter: retail uses CLandCell::find_terrain_poly →
|
||||
// polygon->plane.N.z to get the triangle-specific normal.
|
||||
// SampleNormalZFromHeightmap picks the correct triangle via
|
||||
// the cell's split direction, matching retail + WorldBuilder.
|
||||
if (heightTable is not null && (obj.MinSlope > 0f || obj.MaxSlope < 1f))
|
||||
{
|
||||
float nz = AcDream.Core.Physics.TerrainSurface.SampleNormalZFromHeightmap(
|
||||
block.Height, heightTable,
|
||||
landblockId >> 24, (landblockId >> 16) & 0xFFu,
|
||||
lx, ly);
|
||||
if (nz < obj.MinSlope || nz > obj.MaxSlope) continue;
|
||||
}
|
||||
|
||||
// BaseLoc.Z offset: scenery-specific vertical offset from
|
||||
// the ground (e.g., flowers planted at -0.1m so they
|
||||
// don't float above grass). The renderer adds groundZ
|
||||
// later, so pass the BaseLoc.Z through as-is.
|
||||
float lz = obj.BaseLoc.Origin.Z;
|
||||
|
||||
// Rotation: chunk_005A0000.c lines 4924-4931 (FUN_005a6e60)
|
||||
// Retail calls FUN_00425f10(baseLoc) to copy baseLoc.Orientation
|
||||
// into the frame, THEN calls AFrame::set_heading(degrees).
|
||||
//
|
||||
// set_heading uses yaw = -(450 - heading) % 360 before converting
|
||||
// to a quaternion, which introduces a 90° offset + sign flip
|
||||
// relative to a naive Z rotation. WorldBuilder's
|
||||
// SceneryHelpers.SetHeading reproduces this.
|
||||
//
|
||||
// For objects with Align != 0, retail uses FUN_005a6f60 to
|
||||
// align to the landcell polygon's normal instead of setting
|
||||
// heading from the noise.
|
||||
//
|
||||
// Composition: final = baseLoc.Orientation * headingQuat
|
||||
Quaternion rotation = obj.BaseLoc.Orientation;
|
||||
if (rotation.LengthSquared() < 0.0001f)
|
||||
rotation = Quaternion.Identity;
|
||||
|
||||
if (obj.MaxRotation > 0f)
|
||||
{
|
||||
double rotNoise = unchecked((uint)(1813693831u * globalCellY
|
||||
- (j + 63127u) * (1360117743u * globalCellY * globalCellX + 1888038839u)
|
||||
- 1109124029u * globalCellX)) * 2.3283064e-10;
|
||||
float degrees = (float)(rotNoise * obj.MaxRotation);
|
||||
// AFrame::set_heading transform — matches retail.
|
||||
float yawDeg = -((450f - degrees) % 360f);
|
||||
float yawRad = yawDeg * MathF.PI / 180f;
|
||||
var headingQuat = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, yawRad);
|
||||
rotation = headingQuat * rotation;
|
||||
}
|
||||
|
||||
// Scale: ACViewer Physics/Common/ObjectDesc.cs ScaleObj() (confirmed matches pattern)
|
||||
// offset constant 0x7f51 = 32593 (not in dumped chunks; cross-verified via ACViewer)
|
||||
// same LCG structure as rotation/displacement; uint cast per decompiled normalisation
|
||||
float scale;
|
||||
if (obj.MinScale == obj.MaxScale)
|
||||
{
|
||||
scale = obj.MaxScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
double scaleNoise = unchecked((uint)(1813693831u * globalCellY
|
||||
- (j + 32593u) * (1360117743u * globalCellY * globalCellX + 1888038839u)
|
||||
- 1109124029u * globalCellX)) * 2.3283064e-10;
|
||||
scale = (float)(Math.Pow(obj.MaxScale / obj.MinScale, scaleNoise) * obj.MinScale);
|
||||
}
|
||||
if (scale <= 0) scale = 1f;
|
||||
|
||||
result.Add(new ScenerySpawn(
|
||||
ObjectId: obj.ObjectId,
|
||||
LocalPosition: new Vector3(lx, ly, lz),
|
||||
Rotation: rotation,
|
||||
Scale: scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
// heightTable kept for backward compat; WB path uses
|
||||
// region.LandDefs.LandHeightTable internally via TerrainUtils.GetNormal.
|
||||
_ = heightTable;
|
||||
return GenerateInternal(dats, region, block, landblockId, buildingCells);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase N.1 alternative implementation that delegates the
|
||||
/// algorithm calls to WorldBuilder's <c>SceneryHelpers</c> +
|
||||
/// <c>TerrainUtils</c>. Structurally identical to <see cref="Generate"/>
|
||||
/// but with WB's tested ports doing the work. Selected by
|
||||
/// <see cref="UseWbScenery"/>.
|
||||
/// Returns true if the raw terrain word indicates a road vertex.
|
||||
/// Bits 0-1 of the terrain word encode the road type; any non-zero value
|
||||
/// means the vertex is on a road. Ported from ACViewer GetRoad().
|
||||
/// </summary>
|
||||
private static IReadOnlyList<ScenerySpawn> GenerateViaWb(
|
||||
public static bool IsRoadVertex(ushort raw) => (raw & 0x3u) != 0;
|
||||
|
||||
private static IReadOnlyList<ScenerySpawn> GenerateInternal(
|
||||
DatCollection dats,
|
||||
Region region,
|
||||
LandBlock block,
|
||||
|
|
@ -394,160 +193,4 @@ public static class SceneryGenerator
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the raw terrain word indicates a road vertex.
|
||||
/// Bits 0-1 of the terrain word encode the road type; any non-zero value
|
||||
/// means the vertex is on a road. Ported from ACViewer GetRoad().
|
||||
/// </summary>
|
||||
public static bool IsRoadVertex(ushort raw) => (raw & 0x3u) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Half-width of a road ribbon in world units — the road extends from each
|
||||
/// road vertex by this amount into the neighbor cells. Matches retail's
|
||||
/// `_DAT_007c9cc0 = 5.0f` in FUN_00530d30.
|
||||
/// </summary>
|
||||
private const float RoadHalfWidth = 5.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Retail-faithful road ribbon test — direct port of ACViewer's
|
||||
/// Landblock.OnRoad (Physics/Common/Landblock.cs lines 300-398), which
|
||||
/// itself is a port of CLandBlock::on_road (named-retail 0x0052FFF0).
|
||||
///
|
||||
/// Classifies the 4 corners of the cell containing (lx, ly) by road type
|
||||
/// (bits 0-1 of the terrain word) and applies a different geometric test
|
||||
/// based on which corners are road vertices. Road ribbons have a 5m
|
||||
/// half-width (TileLength - RoadWidth = 19m).
|
||||
/// </summary>
|
||||
internal static bool IsOnRoad(LandBlock block, float lx, float ly)
|
||||
{
|
||||
int x = (int)MathF.Floor(lx / CellSize);
|
||||
int y = (int)MathF.Floor(ly / CellSize);
|
||||
// Clamp so we don't index past the 9x9 terrain grid
|
||||
x = Math.Clamp(x, 0, CellsPerSide - 1);
|
||||
y = Math.Clamp(y, 0, CellsPerSide - 1);
|
||||
|
||||
float rMin = RoadHalfWidth; // 5
|
||||
float rMax = CellSize - RoadHalfWidth; // 19
|
||||
|
||||
// Corner road bits (ACViewer convention):
|
||||
// r0 = (x0, y0) = SW
|
||||
// r1 = (x0, y1) = NW
|
||||
// r2 = (x1, y0) = SE
|
||||
// r3 = (x1, y1) = NE
|
||||
bool r0 = IsRoadVertex(block.Terrain[x * VerticesPerSide + y]);
|
||||
bool r1 = IsRoadVertex(block.Terrain[x * VerticesPerSide + (y + 1)]);
|
||||
bool r2 = IsRoadVertex(block.Terrain[(x + 1) * VerticesPerSide + y]);
|
||||
bool r3 = IsRoadVertex(block.Terrain[(x + 1) * VerticesPerSide + (y + 1)]);
|
||||
|
||||
if (!r0 && !r1 && !r2 && !r3) return false;
|
||||
|
||||
float dx = lx - x * CellSize;
|
||||
float dy = ly - y * CellSize;
|
||||
|
||||
if (r0)
|
||||
{
|
||||
if (r1)
|
||||
{
|
||||
if (r2)
|
||||
{
|
||||
if (r3) return true;
|
||||
return dx < rMin || dy < rMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r3) return dx < rMin || dy > rMax;
|
||||
return dx < rMin;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r2)
|
||||
{
|
||||
if (r3) return dx > rMax || dy < rMin;
|
||||
return dy < rMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r3) return MathF.Abs(dx - dy) < rMin;
|
||||
return dx + dy < rMin;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r1)
|
||||
{
|
||||
if (r2)
|
||||
{
|
||||
if (r3) return dx > rMax || dy > rMax;
|
||||
return MathF.Abs(dx + dy - CellSize) < rMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r3) return dy > rMax;
|
||||
return CellSize + dx - dy < rMin;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r2)
|
||||
{
|
||||
if (r3) return dx > rMax;
|
||||
return CellSize - dx + dy < rMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r3) return CellSize * 2f - dx - dy < rMin;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const int CellsPerSide = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Pseudo-random displacement within a cell for a scenery object. Returns a
|
||||
/// Vector3 in local cell-offset space (the caller adds it to the cell corner
|
||||
/// to get landblock-local position).
|
||||
///
|
||||
/// Verified against decompiled acclient.exe: chunk_005A0000.c lines 4844-4903 (FUN_005a6cc0).
|
||||
/// X offset constant 0xb2cd = 45773; Y offset constant 0x11c0f = 72719.
|
||||
/// Quadrant hash: line 4880; thresholds 0.25/0.5/0.75 map to _DAT_007c97cc/_DAT_007938b8/_DAT_0079c6dc.
|
||||
/// Decompiled normalises signed-int LCG results with "if (val < 0) val += 2^32"; our
|
||||
/// unchecked((uint)(...)) is exactly equivalent.
|
||||
/// </summary>
|
||||
internal static Vector3 DisplaceObject(ObjectDesc obj, uint ix, uint iy, uint iq)
|
||||
{
|
||||
float x, y;
|
||||
var baseLoc = obj.BaseLoc.Origin;
|
||||
|
||||
// X displacement: chunk_005A0000.c lines 4858-4866
|
||||
// iVar4 = (param_3 * 0x6c1ac587 - (param_2 * param_3 * 0x5111bfef + 0x70892fb7) * (param_4 + 0xb2cd)) + param_2 * -0x421be3bd
|
||||
if (obj.DisplaceX <= 0)
|
||||
x = baseLoc.X;
|
||||
else
|
||||
x = (float)(unchecked((uint)(1813693831u * iy - (iq + 45773u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceX + baseLoc.X);
|
||||
|
||||
// Y displacement: chunk_005A0000.c lines 4871-4878 (same structure, offset 0x11c0f = 72719)
|
||||
if (obj.DisplaceY <= 0)
|
||||
y = baseLoc.Y;
|
||||
else
|
||||
y = (float)(unchecked((uint)(1813693831u * iy - (iq + 72719u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceY + baseLoc.Y);
|
||||
|
||||
float z = baseLoc.Z;
|
||||
|
||||
// Quadrant selection: chunk_005A0000.c lines 4880-4902
|
||||
// iVar4 = (param_3 * 0x6c1ac587 - (param_3 * 0x6f7bd965 + 0x421be3bd) * param_2) + -0x17fcedfd
|
||||
// 0x6f7bd965=1870387557, 0x421be3bd=1109124029, -0x17fcedfd → -402451965 (uint: 3892515331)
|
||||
double quadrant = unchecked((uint)(1813693831u * iy - ix * (1870387557u * iy + 1109124029u) - 402451965u)) * 2.3283064e-10;
|
||||
|
||||
if (quadrant >= 0.75) return new Vector3(y, -x, z);
|
||||
if (quadrant >= 0.5) return new Vector3(-x, -y, z);
|
||||
if (quadrant >= 0.25) return new Vector3(-y, x, z);
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue