class CCTLiquid : CCTBase
{
protected static const float HIT_POS_TOLERANCE = 1.0;
protected static const float HIT_POS_TOLERANCE_SQ = HIT_POS_TOLERANCE * HIT_POS_TOLERANCE;
protected static const float SEA_FALLBACK_DISTANCE_BUFFER = 1.5;
protected static const float SYNTHETIC_POND_SAMPLE_STEP = 1.0;
protected static const float SYNTHETIC_POND_SURFACE_TOLERANCE = 0.35;
protected static const float SYNTHETIC_POND_FALLBACK_SURFACE_TOLERANCE = 1.75;
protected static const float SYNTHETIC_CURSOR_DISTANCE_BUFFER = 1.5;
protected static const float SEA_CURSOR_STICK_DISTANCE = 0.75;
protected static const int SEA_CURSOR_STICK_TIME_MS = 350;
protected static const float SYNTHETIC_CURSOR_CACHE_AIM_TOLERANCE = 1.75;
protected static const int SYNTHETIC_CURSOR_CACHE_MAX_AGE_MS = 350;
protected static const float SEA_HIT_RAW_Y_TOLERANCE = 1.25;
protected static const float SEA_CURSOR_MIN_DOWN_DOT = 0.035;
protected static const float LIQUID_SURFACE_RAYCAST_OFFSET = 0.2;
protected vector m_LastSyntheticHitCursor;
protected bool m_HasLastSyntheticHitCursor;
protected int m_LastSyntheticHitCursorTime;
protected float m_MaxVerticalReach;
protected float m_MaxActionDistance;
protected int m_AllowedLiquidSource;
protected ref SurfaceDetectionParameters m_SurfaceDetectionParams;
protected ref SurfaceDetectionParameters m_SurfaceDetectionParamsNoWater;
protected int m_LiquidType = LIQUID_NONE;
protected int m_WaterSourceType = EWaterSourceObjectType.NONE;
protected int m_LiquidSourceType = ELiquidSourceObjectType.NONE;
protected bool m_AllowSeaLevelCursorFallback;
#ifdef DIAG_DEVELOPER
protected bool m_DebugSyntheticCursor;
protected ref array<Shape> m_DebugSyntheticCursorShapes;
#endif
void CCTLiquid(int allowedLiquidSource, float maximalTargetDistance = UAMaxDistances.DEFAULT, float maxVerticalReach = UAMaxDistances.DEFAULT, SurfaceDetectionType surfaceDetectionType = SurfaceDetectionType.Roadway, bool allowSeaLevelCursorFallback = false)
{
m_LastSyntheticHitCursor = vector.Zero;
m_HasLastSyntheticHitCursor = false;
m_LastSyntheticHitCursorTime = 0;
m_MaxActionDistance = maximalTargetDistance;
m_MaxVerticalReach = maxVerticalReach;
m_AllowedLiquidSource = allowedLiquidSource;
m_AllowSeaLevelCursorFallback = allowSeaLevelCursorFallback;
m_SurfaceDetectionParams = new SurfaceDetectionParameters();
m_SurfaceDetectionParams.type = surfaceDetectionType;
m_SurfaceDetectionParams.includeWater = true;
m_SurfaceDetectionParams.syncMode = UseObjectsMode.NoWait;
m_SurfaceDetectionParams.rsd = RoadSurfaceDetection.CLOSEST;
m_SurfaceDetectionParamsNoWater = new SurfaceDetectionParameters();
m_SurfaceDetectionParamsNoWater.type = surfaceDetectionType;
m_SurfaceDetectionParamsNoWater.includeWater = false;
m_SurfaceDetectionParamsNoWater.syncMode = UseObjectsMode.NoWait;
m_SurfaceDetectionParamsNoWater.rsd = RoadSurfaceDetection.CLOSEST;
#ifdef DIAG_DEVELOPER
m_DebugSyntheticCursor = DiagMenu.GetBool(DiagMenuIDs.SYNTHETIC_FISHING_CURSOR);
m_DebugSyntheticCursorShapes = new array<Shape>();
#endif
}
override bool Can(PlayerBase player, ActionTarget target)
{
if (!target)
return false;
m_LiquidType = LIQUID_NONE;
m_WaterSourceType = EWaterSourceObjectType.NONE;
m_LiquidSourceType = ELiquidSourceObjectType.NONE;
vector playerPos = player.GetPosition();
Object targetObj = target.GetObject();
vector objectPosition = vector.Zero;
bool isValidInteract = false;
//! Pre-calculate object validity once.
if (targetObj)
{
m_WaterSourceType = targetObj.GetWaterSourceObjectType();
m_LiquidSourceType = targetObj.GetLiquidSourceObjectType();
if (targetObj.IsWell())
{
TIntArray componentIndices = new TIntArray();
targetObj.GetActionComponentsForSelectionName(targetObj.GetViewGeometryLevel(), UAMisc.LIQUID_SOURCE_SELECTION, componentIndices);
if (componentIndices.Count() > 0)
isValidInteract = componentIndices.Find(target.GetComponentIndex()) != -1;
else
isValidInteract = true;
}
else
{
objectPosition = targetObj.GetPosition();
isValidInteract = IsValidTargetObject(playerPos, objectPosition, m_WaterSourceType, m_LiquidSourceType, target.IsProxy());
}
}
vector hitPosition = target.GetCursorHitPos();
#ifdef DIAG_DEVELOPER
m_DebugSyntheticCursor = DiagMenu.GetBool(DiagMenuIDs.SYNTHETIC_FISHING_CURSOR);
#endif
//! Fishing/synthetic/sea-normalized cursor path
//! For fishing or other synthetic cursor actions, do not fall through to generic surface checks. If the synthetic cursor cannot be resolved, the action should not be available.
if (m_AllowSeaLevelCursorFallback && !targetObj)
{
vector resolvedHitPosition;
int resolvedLiquidType;
if (TryResolveSyntheticCursor(player, playerPos, hitPosition, resolvedHitPosition, resolvedLiquidType))
{
hitPosition = resolvedHitPosition;
target.SetCursorHitPos(hitPosition);
m_LiquidType = resolvedLiquidType;
return IsValidLiquid(m_LiquidType);
}
}
if (isValidInteract && targetObj)
{
m_LiquidType = targetObj.GetLiquidSourceType();
return IsValidLiquid(m_LiquidType);
}
string surfaceType;
if (CheckSurfaceLiquid(player, targetObj, hitPosition, playerPos, isValidInteract, surfaceType))
{
return IsValidLiquid(m_LiquidType);
}
if (!targetObj && surfaceType == string.Empty && m_LiquidType == LIQUID_NONE)
{
if (!CheckSeaLevelFallback(hitPosition, playerPos))
{
#ifdef DIAG_DEVELOPER
DbgUI.Begin("", 10, 200);
if (m_DebugSyntheticCursor)
{
DbgUI.Text(string.Format("CCTLiquid CheckSeaLevelFallback rejected: reason=could not create sea hit point | hitPosition=%1 | playerPos=%2", hitPosition, playerPos));
}
DbgUI.End();
#endif
}
}
return IsValidLiquid(m_LiquidType);
}
protected bool IsValidLiquid(int liquid)
{
return (liquid & m_AllowedLiquidSource) != 0;
}
protected bool IsValidTargetObject(vector playerPos, vector objectPosition, EWaterSourceObjectType waterSourceType, ELiquidSourceObjectType liquidSourceType, bool isProxy)
{
//! Early exit for invalid types
if ((waterSourceType == EWaterSourceObjectType.NONE && liquidSourceType == ELiquidSourceObjectType.NONE) || isProxy)
return false;
//! Check if position is valid
if (IsNearlyZero(objectPosition))
return false;
//! Single distance calculation
if (vector.Distance(playerPos, objectPosition) > m_MaxActionDistance)
return false;
//! Check vertical distance
float heightDiff = Math.AbsFloat(objectPosition[1] - playerPos[1]);
return heightDiff <= m_MaxVerticalReach;
}
protected bool CheckSurfaceLiquid(PlayerBase player, Object targetObj, vector hitPosition, vector playerPos, bool isValidInteract, out string surfaceType)
{
surfaceType = string.Empty;
//! Valid object source wins, do not also check surface.
if (isValidInteract)
return false;
//! Preserve ValidateDistances() range check for surface-liquid hits.
if (IsNearlyZero(hitPosition))
return false;
m_SurfaceDetectionParams.position = hitPosition;
SurfaceDetectionResult surfaceResult = new SurfaceDetectionResult();
if (!g_Game.GetSurface(m_SurfaceDetectionParams, surfaceResult))
return false;
if (!surfaceResult)
return false;
SurfaceInfo surfaceInfo = surfaceResult.surface;
if (!surfaceInfo)
return false;
surfaceType = surfaceInfo.GetSurfaceType();
if (surfaceType == string.Empty)
return false;
float surfaceHeight = surfaceResult.height;
float heightDiffSurface = Math.AbsFloat(hitPosition[1] - surfaceHeight);
float playerToSurfaceHeight = Math.AbsFloat(playerPos[1] - surfaceHeight);
int liquidType = g_Game.CorrectLiquidType(surfaceInfo.GetLiquidType());
if (liquidType == LIQUID_NONE)
return false;
vector liquidSurfacePos = hitPosition;
liquidSurfacePos[1] = surfaceHeight;
//! Do not allow water merely below grass/terrain.
if (!IsCursorActuallyOnLiquidSurface(hitPosition, liquidSurfacePos))
return false;
vector surfacePos = liquidSurfacePos;
surfacePos[1] = surfacePos[1] + LIQUID_SURFACE_RAYCAST_OFFSET;
if (vector.Distance(playerPos, surfacePos) > m_MaxActionDistance)
return false;
if (targetObj)
{
if (heightDiffSurface > 0.25)
return false;
if (!HasClearPathToLiquidSurface(player, surfacePos))
return false;
}
else
{
if (!HasClearPathToLiquidSurface(player, surfacePos))
return false;
}
if (playerToSurfaceHeight > m_MaxVerticalReach)
return false;
m_LiquidType = liquidType;
return true;
}
protected bool HasClearPathToLiquidSurface(PlayerBase player, vector surfacePos)
{
vector rayStart;
MiscGameplayFunctions.GetHeadBonePos(player, rayStart);
RaycastRVParams rayInput = new RaycastRVParams(rayStart, surfacePos, player);
rayInput.flags = CollisionFlags.FIRSTCONTACT;
rayInput.type = ObjIntersectFire;
rayInput.sorted = true;
rayInput.ignore = player;
array<ref RaycastRVResult> results = new array<ref RaycastRVResult>();
if (!DayZPhysics.RaycastRVProxy(rayInput, results))
return true;
float surfaceDist = vector.Distance(rayStart, surfacePos);
foreach (RaycastRVResult result : results)
{
if (!result)
continue;
Object hitObj = result.obj;
if (!hitObj)
continue;
float hitDist = vector.Distance(rayStart, result.pos);
//! Something solid is between player and the liquid point.
if (hitDist < surfaceDist)
return false;
}
return true;
}
protected bool CheckSeaLevelFallback(vector hitPosition, vector playerPos)
{
vector normalizedSeaHit;
if (!TryGetNormalizedSeaHitPoint(playerPos, hitPosition, normalizedSeaHit))
return false;
m_LiquidType = LIQUID_SALTWATER;
return true;
}
protected bool TryGetLiquidSurfaceAt(vector probePosition, out vector surfacePosition, out int liquidType, out string surfaceType)
{
surfacePosition = vector.Zero;
liquidType = LIQUID_NONE;
surfaceType = string.Empty;
m_SurfaceDetectionParams.position = probePosition;
SurfaceDetectionResult surfaceResult = new SurfaceDetectionResult();
if (!g_Game.GetSurface(m_SurfaceDetectionParams, surfaceResult))
return false;
if (!surfaceResult)
return false;
SurfaceInfo surfaceInfo = surfaceResult.surface;
if (!surfaceInfo)
return false;
surfaceType = surfaceInfo.GetSurfaceType();
if (surfaceType == string.Empty)
return false;
liquidType = g_Game.CorrectLiquidType(surfaceInfo.GetLiquidType());
if (liquidType == LIQUID_NONE)
return false;
surfacePosition = probePosition;
surfacePosition[1] = surfaceResult.height;
return true;
}
protected bool TryBuildSeaLevelHitPosition(PlayerBase player, vector playerPos, out vector seaHitPosition)
{
seaHitPosition = vector.Zero;
//! Server validates the synced cursor position. It should not invent one from camera data.
if (g_Game.IsHeadlessOrDedicatedServer())
return false;
float seaLevel = g_Game.SurfaceGetSeaLevel();
float seaLevelMax = g_Game.SurfaceGetSeaLevelMax();
if (playerPos[1] > seaLevelMax + m_MaxVerticalReach)
return false;
vector cameraPos = g_Game.GetCurrentCameraPosition();
vector cameraDir = g_Game.GetCurrentCameraDirection();
if (cameraPos == vector.Zero || cameraDir == vector.Zero)
return false;
cameraDir.Normalize();
float safeDistance = m_MaxActionDistance - SEA_FALLBACK_DISTANCE_BUFFER;
if (safeDistance <= 1.0)
safeDistance = m_MaxActionDistance * 0.8;
//! Dynamic minimum downward angle. If the ray is too close to horizontal, tiny camera sway causes huge sea cursor jumps.
float cameraToSeaHeight = cameraPos[1] - seaLevel;
if (cameraToSeaHeight <= 0.0)
return false;
//! Only reject near-horizontal aim. Do not scale this directly with height, because the real intersection check below decides whether the sea-plane hit is within range.
float minStableDownDot = SEA_CURSOR_MIN_DOWN_DOT;
float maxAllowedCameraDirY = -minStableDownDot;
if (cameraDir[1] > maxAllowedCameraDirY)
{
#ifdef DIAG_DEVELOPER
DbgUI.Begin("", 10, 200);
if (m_DebugSyntheticCursor)
{
DbgUI.Text(string.Format("CCTLiquid TryBuildSeaLevelHitPosition rejected: reason=camera too shallow | cameraDirY=%1 | minStableDownDot=%2 | height=%3 | safeDist=%4", cameraDir[1], minStableDownDot, cameraToSeaHeight, safeDistance));
}
DbgUI.End();
#endif
return false;
}
//! Real intersection of camera ray with sea plane.
float t = (seaLevel - cameraPos[1]) / cameraDir[1];
if (t <= 0)
return false;
vector candidate = cameraPos + cameraDir * t;
candidate[1] = seaLevel;
float safeDistanceSq = safeDistance * safeDistance;
//! If the real cursor/sea intersection is beyond allowed range, do not clamp a closer fake point.
if (vector.DistanceSq(playerPos, candidate) > safeDistanceSq)
{
#ifdef DIAG_DEVELOPER
DbgUI.Begin("", 20, 200);
if (m_DebugSyntheticCursor)
{
DbgUI.Text(string.Format("CCTLiquid TryBuildSeaLevelHitPosition rejected: reason=Real sea intersection too far | player=%1 | candidate=%2 | dist=%3 | safeDist=%4", playerPos.ToString(), candidate.ToString(), vector.Distance(playerPos, candidate), safeDistance));
}
DbgUI.End();
#endif
return false;
}
if (!IsSeaHitPositionValid(playerPos, candidate))
return false;
vector seaSurfacePos = candidate;
seaSurfacePos[1] = seaSurfacePos[1] + LIQUID_SURFACE_RAYCAST_OFFSET;
if (!HasClearPathToLiquidSurface(player, seaSurfacePos))
return false;
//! Apply small deadband/sticky cache.
candidate = ApplySyntheticHitCursorStability(playerPos, candidate, LIQUID_SALTWATER);
if (!IsSeaHitPositionValid(playerPos, candidate))
return false;
vector cachedOrStableSeaSurfacePos = candidate;
cachedOrStableSeaSurfacePos[1] = cachedOrStableSeaSurfacePos[1] + LIQUID_SURFACE_RAYCAST_OFFSET;
if (!HasClearPathToLiquidSurface(player, cachedOrStableSeaSurfacePos))
return false;
seaHitPosition = candidate;
return true;
}
protected bool IsSeaHitPositionValid(vector playerPos, vector hitPosition)
{
vector normalizedHit;
if (!TryGetNormalizedSeaHitPoint(playerPos, hitPosition, normalizedHit))
return false;
return true;
}
protected bool TryBuildPondHitPosition(PlayerBase player, vector playerPos, out vector pondHitPosition)
{
#ifdef DIAG_DEVELOPER
float debugBestDelta = 999999.0;
int debugSampleCount = 0;
int debugValidKnownSurfaceCount = 0;
string debugPondFailReason = "no pond samples";
#endif
pondHitPosition = vector.Zero;
//! Client only. Server validates synced cursor position.
if (g_Game.IsHeadlessOrDedicatedServer())
return false;
vector cameraPos = g_Game.GetCurrentCameraPosition();
vector cameraDir = g_Game.GetCurrentCameraDirection();
if (cameraPos == vector.Zero || cameraDir == vector.Zero)
return false;
cameraDir.Normalize();
//! If looking upward/horizontal, we are probably not aiming at a pond surface.
if (cameraDir[1] > 0.15)
return false;
float safeDistance = m_MaxActionDistance - SYNTHETIC_CURSOR_DISTANCE_BUFFER;
if (safeDistance <= 1.0)
safeDistance = m_MaxActionDistance * 0.8;
vector bestSurfacePos = vector.Zero;
int bestLiquidType = LIQUID_NONE;
float bestDelta = 999999.0;
vector pondSurfacePos = vector.Zero;
for (float dist = 1.0; dist <= safeDistance; dist += SYNTHETIC_POND_SAMPLE_STEP)
{
#ifdef DIAG_DEVELOPER
debugSampleCount++;
#endif
vector probePos = cameraPos + cameraDir * dist;
vector surfacePos;
int liquidType;
string surfaceType;
if (!TryGetLiquidSurfaceAt(probePos, surfacePos, liquidType, surfaceType))
{
#ifdef DIAG_DEVELOPER
debugPondFailReason = "TryGetLiquidSurfaceAt failed";
#endif
continue;
}
if (!IsAllowedPondLiquidSurface(playerPos, surfacePos, liquidType))
{
#ifdef DIAG_DEVELOPER
debugPondFailReason = "known pond surface invalid";
#endif
continue;
}
#ifdef DIAG_DEVELOPER
debugValidKnownSurfaceCount++;
#endif
float rayToSurfaceDelta = Math.AbsFloat(probePos[1] - surfacePos[1]);
if (rayToSurfaceDelta < bestDelta)
{
bestDelta = rayToSurfaceDelta;
bestSurfacePos = surfacePos;
bestLiquidType = liquidType;
#ifdef DIAG_DEVELOPER
debugBestDelta = bestDelta;
debugPondFailReason = "best pond surface too far from camera ray";
#endif
}
//! The ray is close enough to the liquid surface.
if (rayToSurfaceDelta <= SYNTHETIC_POND_SURFACE_TOLERANCE)
{
surfacePos = ApplySyntheticHitCursorStability(playerPos, surfacePos, liquidType);
if (!IsValidPondHitPosition(playerPos, surfacePos, liquidType))
continue;
pondSurfacePos = surfacePos;
pondSurfacePos[1] = pondSurfacePos[1] + LIQUID_SURFACE_RAYCAST_OFFSET;
if (!HasClearPathToLiquidSurface(player, pondSurfacePos))
continue;
pondHitPosition = surfacePos;
return true;
}
}
//! Last chance: use closest sampled surface if it was very close.
if (!IsNearlyZero(bestSurfacePos) && bestDelta <= SYNTHETIC_POND_FALLBACK_SURFACE_TOLERANCE)
{
bestSurfacePos = ApplySyntheticHitCursorStability(playerPos, bestSurfacePos, bestLiquidType);
if (IsValidPondHitPosition(playerPos, bestSurfacePos, bestLiquidType))
{
pondSurfacePos = bestSurfacePos;
pondSurfacePos[1] = pondSurfacePos[1] + LIQUID_SURFACE_RAYCAST_OFFSET;
if (!HasClearPathToLiquidSurface(player, pondSurfacePos))
return false;
pondHitPosition = bestSurfacePos;
return true;
}
}
#ifdef DIAG_DEVELOPER
DbgUI.Begin("", 50, 240);
if (m_DebugSyntheticCursor)
{
DbgUI.Text(string.Format("CCTLiquid TryBuildPondHitPosition failed: reason=%1 | samples=%2 | validSurfaces=%3 | bestDelta=%4 | tolerance=%5 | relaxedTolerance=%6", debugPondFailReason, debugSampleCount, debugValidKnownSurfaceCount, debugBestDelta, SYNTHETIC_POND_SURFACE_TOLERANCE, SYNTHETIC_POND_FALLBACK_SURFACE_TOLERANCE));
}
DbgUI.End();
#endif
return false;
}
protected bool IsValidPondHitPosition(vector playerPos, vector hitPosition, int liquidType = LIQUID_NONE)
{
vector surfacePos;
string surfaceType;
int validatedLiquidType;
return TryValidatePondHitPosition(playerPos, hitPosition, surfacePos, validatedLiquidType, surfaceType, liquidType);
}
protected bool TryGetNormalizedSeaHitPoint(vector playerPos, vector hitPosition, out vector normalizedHit)
{
normalizedHit = vector.Zero;
if (IsNearlyZero(hitPosition))
return false;
if (!g_Game.SurfaceIsSea(hitPosition[0], hitPosition[2]))
return false;
float seaLevel = g_Game.SurfaceGetSeaLevel();
float seaLevelMax = g_Game.SurfaceGetSeaLevelMax();
//! Raw Y sanity check. Do not require exact sea Y here because synced cursor Y can differ.
float hitToSeaHeight = Math.AbsFloat(hitPosition[1] - seaLevel);
if (hitToSeaHeight > SEA_HIT_RAW_Y_TOLERANCE)
return false;
if (hitPosition[1] > seaLevelMax + m_MaxVerticalReach)
return false;
normalizedHit = hitPosition;
normalizedHit[1] = seaLevel;
//! Validate real fishing distance against normalized sea point.
if (vector.Distance(playerPos, normalizedHit) > m_MaxActionDistance)
return false;
//! Preserve ValidateDistances() vertical behavior for normalized sea hits.
float playerToSeaHeight = Math.AbsFloat(playerPos[1] - seaLevel);
if (playerToSeaHeight > m_MaxVerticalReach)
return false;
return true;
}
protected bool TryBuildSyntheticCursor(PlayerBase player, vector playerPos, out vector syntheticHitPosition, out int syntheticLiquidType)
{
syntheticHitPosition = vector.Zero;
syntheticLiquidType = LIQUID_NONE;
//! Client only. Server validates synced cursor position.
if (g_Game.IsHeadlessOrDedicatedServer())
return false;
if ((m_AllowedLiquidSource & LIQUID_SALTWATER) != 0)
{
if (TryBuildSeaLevelHitPosition(player, playerPos, syntheticHitPosition))
{
syntheticLiquidType = LIQUID_SALTWATER;
return true;
}
}
if ((m_AllowedLiquidSource & LIQUID_FRESHWATER) != 0)
{
if (TryBuildPondHitPosition(player, playerPos, syntheticHitPosition))
{
//! Let final validation detect exact liquid type.
syntheticLiquidType = LIQUID_FRESHWATER;
return true;
}
}
return false;
}
protected bool TryResolveSyntheticCursor(PlayerBase player, vector playerPos, vector currentHitPosition, out vector resolvedHitPosition, out int resolvedLiquidType)
{
resolvedHitPosition = currentHitPosition;
resolvedLiquidType = LIQUID_NONE;
//! First validate the existing/synced cursor.
if (TryValidateHitCursor(player, playerPos, currentHitPosition, resolvedHitPosition, resolvedLiquidType))
return true;
//! If there is a real cursor hit but it is not valid liquid, do not synthesize water. This prevents looking at ground/terrain near a pond from creating a synthetic cursor.
if (currentHitPosition != vector.Zero)
{
ClearSyntheticCursorCache();
#ifdef DIAG_DEVELOPER
if (m_DebugSyntheticCursor)
DebugDrawSyntheticCursor(player, currentHitPosition, vector.Zero, false, "current non-liquid hit prevents synthetic cursor", LIQUID_NONE);
else
ClearDebugCursorShapes();
#endif
return false;
}
//! Server stops here. It should not invent a cursor from camera state.
if (g_Game.IsHeadlessOrDedicatedServer())
return false;
//! Client fallback: build synthetic cursor only when current cursor is invalid.
vector syntheticHitPosition;
int syntheticLiquidType;
if (!TryBuildSyntheticCursor(player, playerPos, syntheticHitPosition, syntheticLiquidType))
{
//! Do not immediately drop a previously valid cursor. A single frame can fail because the camera ray is shallow or surface detection missed.
if (TryUseCachedSyntheticCursor(player, playerPos, currentHitPosition, resolvedHitPosition, resolvedLiquidType))
{
#ifdef DIAG_DEVELOPER
if (m_DebugSyntheticCursor)
DebugDrawSyntheticCursor(player, currentHitPosition, resolvedHitPosition, true, "accepted cached synthetic cursor", resolvedLiquidType);
else
ClearDebugCursorShapes();
#endif
return true;
}
ClearSyntheticCursorCache();
#ifdef DIAG_DEVELOPER
if (m_DebugSyntheticCursor)
DebugDrawSyntheticCursor(player, currentHitPosition, syntheticHitPosition, false, "synthetic cursor failed", LIQUID_NONE);
else
ClearDebugCursorShapes();
#endif
return false;
}
//! Validate the generated cursor once.
if (!TryValidateHitCursor(player, playerPos, syntheticHitPosition, resolvedHitPosition, resolvedLiquidType))
{
ClearSyntheticCursorCache();
#ifdef DIAG_DEVELOPER
if (m_DebugSyntheticCursor)
DebugDrawSyntheticCursor(player, currentHitPosition, syntheticHitPosition, false, "synthetic cursor invalid after build", syntheticLiquidType);
else
ClearDebugCursorShapes();
#endif
return false;
}
#ifdef DIAG_DEVELOPER
if (m_DebugSyntheticCursor)
DebugDrawSyntheticCursor(player, currentHitPosition, resolvedHitPosition, true, "accepted synthetic cursor", resolvedLiquidType);
else
ClearDebugCursorShapes();
#endif
return true;
}
protected bool TryValidatePondHitPosition(vector playerPos, vector hitPosition, out vector surfacePos, out int liquidType, out string surfaceType, int expectedLiquidType = LIQUID_NONE)
{
surfacePos = vector.Zero;
liquidType = LIQUID_NONE;
surfaceType = string.Empty;
if (IsNearlyZero(hitPosition))
return false;
if (vector.Distance(playerPos, hitPosition) > m_MaxActionDistance)
return false;
if (!g_Game.SurfaceIsPond(hitPosition[0], hitPosition[2]))
return false;
int detectedLiquidType;
if (!TryGetLiquidSurfaceAt(hitPosition, surfacePos, detectedLiquidType, surfaceType))
return false;
//! Use detected liquid as the source of truth.
liquidType = detectedLiquidType;
//! If caller expected a specific liquid mask/type, require compatibility.
if (expectedLiquidType != LIQUID_NONE && (liquidType & expectedLiquidType) == 0)
return false;
if (!IsAllowedPondLiquidSurface(playerPos, surfacePos, liquidType))
return false;
float hitToSurfaceHeight = Math.AbsFloat(hitPosition[1] - surfacePos[1]);
if (hitToSurfaceHeight > SYNTHETIC_POND_SURFACE_TOLERANCE)
return false;
return true;
}
protected void ClearSyntheticCursorCache()
{
m_LastSyntheticHitCursor = vector.Zero;
m_HasLastSyntheticHitCursor = false;
m_LastSyntheticHitCursorTime = 0;
}
protected vector ApplySyntheticHitCursorStability(vector playerPos, vector candidate, int liquidType)
{
int now = g_Game.GetTime();
if (m_HasLastSyntheticHitCursor)
{
int age = now - m_LastSyntheticHitCursorTime;
if (age <= SEA_CURSOR_STICK_TIME_MS)
{
float stickDistSq = SEA_CURSOR_STICK_DISTANCE * SEA_CURSOR_STICK_DISTANCE;
float deltaSq = vector.DistanceSq(candidate, m_LastSyntheticHitCursor);
if (deltaSq <= stickDistSq)
{
if (liquidType == LIQUID_SALTWATER && IsSeaHitPositionValid(playerPos, m_LastSyntheticHitCursor))
{
m_LastSyntheticHitCursorTime = now;
return m_LastSyntheticHitCursor;
}
if (liquidType != LIQUID_SALTWATER && IsValidPondHitPosition(playerPos, m_LastSyntheticHitCursor, liquidType))
{
m_LastSyntheticHitCursorTime = now;
return m_LastSyntheticHitCursor;
}
}
}
}
m_LastSyntheticHitCursor = candidate;
m_HasLastSyntheticHitCursor = true;
m_LastSyntheticHitCursorTime = now;
return candidate;
}
protected bool TryValidateHitCursor(PlayerBase player, vector playerPos, vector hitPosition, out vector resolvedHitPosition, out int resolvedLiquidType)
{
resolvedHitPosition = hitPosition;
resolvedLiquidType = LIQUID_NONE;
if (IsNearlyZero(hitPosition))
return false;
//! Saltwater path. This normalizes synced sea hits like <x, 0.546, z> back onto the sea plane.
if ((m_AllowedLiquidSource & LIQUID_SALTWATER) != 0)
{
vector normalizedSeaHit;
if (TryGetNormalizedSeaHitPoint(playerPos, hitPosition, normalizedSeaHit))
{
if (!HasClearPathToLiquidSurface(player, normalizedSeaHit))
return false;
if (!IsCursorActuallyOnLiquidSurface(hitPosition, normalizedSeaHit))
return false;
resolvedHitPosition = normalizedSeaHit;
resolvedLiquidType = LIQUID_SALTWATER;
return true;
}
}
//! Freshwater / pond path.
if ((m_AllowedLiquidSource & LIQUID_FRESHWATER) != 0)
{
vector surfacePos;
string surfaceType;
int detectedLiquidType;
if (TryValidatePondHitPosition(playerPos, hitPosition, surfacePos, detectedLiquidType, surfaceType))
{
if (!HasClearPathToLiquidSurface(player, surfacePos))
return false;
if (!IsCursorActuallyOnLiquidSurface(hitPosition, surfacePos))
return false;
resolvedHitPosition = surfacePos;
resolvedLiquidType = detectedLiquidType;
return true;
}
}
return false;
}
protected bool TryUseCachedSyntheticCursor(PlayerBase player, vector playerPos, vector currentHitPosition, out vector resolvedHitPosition, out int resolvedLiquidType)
{
resolvedHitPosition = vector.Zero;
resolvedLiquidType = LIQUID_NONE;
if (!m_HasLastSyntheticHitCursor)
return false;
int now = g_Game.GetTime();
int age = now - m_LastSyntheticHitCursorTime;
//! Cache is only a short grace fallback, not a persistent targeting source.
if (age > SYNTHETIC_CURSOR_CACHE_MAX_AGE_MS)
return false;
//! Important: cached cursor must still match current aim.
if (!IsCachedSyntheticCursorRelevant(currentHitPosition, m_LastSyntheticHitCursor))
return false;
if (TryValidateHitCursor(player, playerPos, m_LastSyntheticHitCursor, resolvedHitPosition, resolvedLiquidType))
{
m_LastSyntheticHitCursorTime = now;
return true;
}
return false;
}
protected bool IsPositionNearCurrentCameraRay(vector position, float maxPerpendicularDistance)
{
if (position == vector.Zero)
return false;
vector cameraPos = g_Game.GetCurrentCameraPosition();
vector cameraDir = g_Game.GetCurrentCameraDirection();
if (cameraPos == vector.Zero || cameraDir == vector.Zero)
return false;
cameraDir.Normalize();
vector cameraToPosition = position - cameraPos;
float projectedDistance = vector.Dot(cameraToPosition, cameraDir);
//! Cached point must be in front of camera.
if (projectedDistance <= 0.0)
return false;
float safeDistance = m_MaxActionDistance - SYNTHETIC_CURSOR_DISTANCE_BUFFER;
if (safeDistance <= 1.0)
safeDistance = m_MaxActionDistance * 0.8;
//! Do not allow using a cached point far beyond the active aim range.
if (projectedDistance > safeDistance + maxPerpendicularDistance)
return false;
vector closestPointOnRay = cameraPos + cameraDir * projectedDistance;
float maxPerpendicularDistanceSq = maxPerpendicularDistance * maxPerpendicularDistance;
return vector.DistanceSq(position, closestPointOnRay) <= maxPerpendicularDistanceSq;
}
protected bool IsCachedSyntheticCursorRelevant(vector currentHitPosition, vector cachedHitPosition)
{
if (cachedHitPosition == vector.Zero)
return false;
float aimToleranceSq = SYNTHETIC_CURSOR_CACHE_AIM_TOLERANCE * SYNTHETIC_CURSOR_CACHE_AIM_TOLERANCE;
//! If there is a current real hit, cached point must be near that hit. If it is not, do not let an old water point keep the action alive.
if (currentHitPosition != vector.Zero)
{
return vector.DistanceSq(currentHitPosition, cachedHitPosition) <= aimToleranceSq;
}
//! If there is no real hit, compare cached point to current camera ray.
return IsPositionNearCurrentCameraRay(cachedHitPosition, SYNTHETIC_CURSOR_CACHE_AIM_TOLERANCE);
}
protected bool IsAllowedPondLiquidSurface(vector playerPos, vector surfacePos, int liquidType)
{
if (liquidType == LIQUID_NONE || liquidType == LIQUID_SALTWATER)
return false;
if ((liquidType & m_AllowedLiquidSource) == 0)
return false;
if (!g_Game.SurfaceIsPond(surfacePos[0], surfacePos[2]))
return false;
if (vector.Distance(playerPos, surfacePos) > m_MaxActionDistance)
return false;
float playerToSurfaceHeight = Math.AbsFloat(playerPos[1] - surfacePos[1]);
if (playerToSurfaceHeight > m_MaxVerticalReach)
return false;
return true;
}
protected bool TryGetNonWaterSurfaceAt(vector probePosition, out vector surfacePosition)
{
surfacePosition = vector.Zero;
m_SurfaceDetectionParamsNoWater.position = probePosition;
m_SurfaceDetectionParamsNoWater.rsd = RoadSurfaceDetection.CLOSEST;
SurfaceDetectionResult surfaceResult = new SurfaceDetectionResult();
if (!g_Game.GetSurface(m_SurfaceDetectionParamsNoWater, surfaceResult))
return false;
if (!surfaceResult)
return false;
SurfaceInfo surfaceInfo = surfaceResult.surface;
if (!surfaceInfo)
return false;
string surfaceType = surfaceInfo.GetSurfaceType();
if (surfaceType == string.Empty)
return false;
int surfaceLiquid = surfaceInfo.GetLiquidType();
//! Check if we have the same or an other liquid surface
if (surfaceLiquid > LIQUID_NONE)
return false;
surfacePosition = probePosition;
surfacePosition[1] = surfaceResult.height;
return true;
}
protected bool IsCursorActuallyOnLiquidSurface(vector hitPosition, vector liquidSurfacePos)
{
if (IsNearlyZero(hitPosition))
return false;
if (IsNearlyZero(liquidSurfacePos))
return false;
if (hitPosition[1] >= liquidSurfacePos[1])
return false;
vector nonWaterSurfacePos;
if (TryGetNonWaterSurfaceAt(hitPosition, nonWaterSurfacePos))
{
//! If terrain/grass/road surface is higher, the player is aiming at ground over/near water, not the water itself.
if (nonWaterSurfacePos[1] > liquidSurfacePos[1])
return false;
}
return true;
}
override bool CanContinue(PlayerBase player, ActionTarget target)
{
return Can(player, target); //! Re-validate during action
}
int GetLiquidType()
{
return m_LiquidType;
}
int GetWaterSourceType()
{
return m_WaterSourceType;
}
int GetLiquidSourceType()
{
return m_LiquidSourceType;
}
bool IsNearlyZero(vector value)
{
return vector.DistanceSq(value, vector.Zero) < 0.0001;
}
#ifdef DIAG_DEVELOPER
protected void ClearDebugCursorShapes()
{
if (!m_DebugSyntheticCursorShapes)
return;
for (int i = 0; i < m_DebugSyntheticCursorShapes.Count(); i++)
{
Shape shape = m_DebugSyntheticCursorShapes[i];
if (shape)
Debug.RemoveShape(shape);
}
m_DebugSyntheticCursorShapes.Clear();
}
protected void DebugDrawSyntheticCursor(PlayerBase player, vector originalHitPosition, vector cursorHitPosition, bool accepted, string reason, int liquidType = LIQUID_NONE)
{
ClearDebugCursorShapes();
if (!player)
return;
vector playerPos = player.GetPosition();
vector headPos;
MiscGameplayFunctions.GetHeadBonePos(player, headPos);
int colorAccepted = ARGB(255, 0, 255, 64);
int colorRejected = ARGB(255, 255, 32, 32);
int colorLine = ARGB(255, 0, 180, 255);
int colorOriginal = ARGB(255, 255, 255, 0);
int finalColor;
if (accepted)
finalColor = colorAccepted;
else
finalColor = colorRejected;
//! Main synthetic cursor point
if (!IsNearlyZero(cursorHitPosition))
{
m_DebugSyntheticCursorShapes.Insert(Debug.DrawSphere(cursorHitPosition, 0.35, finalColor, ShapeFlags.NOZBUFFER));
m_DebugSyntheticCursorShapes.Insert(Debug.DrawLine(headPos, cursorHitPosition, colorLine));
//! Small cross on the water plane
vector x1 = cursorHitPosition;
vector x2 = cursorHitPosition;
vector z1 = cursorHitPosition;
vector z2 = cursorHitPosition;
x1[0] = x1[0] - 0.5;
x2[0] = x2[0] + 0.5;
z1[2] = z1[2] - 0.5;
z2[2] = z2[2] + 0.5;
m_DebugSyntheticCursorShapes.Insert(Debug.DrawLine(x1, x2, finalColor));
m_DebugSyntheticCursorShapes.Insert(Debug.DrawLine(z1, z2, finalColor));
}
//! Original hit position, if any
if (!IsNearlyZero(originalHitPosition))
{
m_DebugSyntheticCursorShapes.Insert(Debug.DrawSphere(originalHitPosition, 0.2, colorOriginal, ShapeFlags.NOZBUFFER));
m_DebugSyntheticCursorShapes.Insert(Debug.DrawLine(playerPos, originalHitPosition, colorOriginal));
}
float dist = 0.0;
bool isSea = false;
bool isPond = false;
if (!IsNearlyZero(cursorHitPosition))
{
dist = vector.Distance(playerPos, cursorHitPosition);
isSea = g_Game.SurfaceIsSea(cursorHitPosition[0], cursorHitPosition[2]);
isPond = g_Game.SurfaceIsPond(cursorHitPosition[0], cursorHitPosition[2]);
}
DbgUI.Begin("", 30, 200);
{
DbgUI.Text(string.Format("CCTLiquid TryResolveSyntheticCursor: accepted=%1 | reason=%2 | liquid=%3 | player=%4 | originalHit=%5 | resolvedHit=%6 | dist=%7 | isSea=%8 | isPond=%9", accepted, reason, liquidType, playerPos.ToString(), originalHitPosition.ToString(), cursorHitPosition.ToString(), dist, isSea, isPond));
}
DbgUI.End();
}
#endif
}