fixes 2
This commit is contained in:
@@ -196,7 +196,8 @@ out skel qt;
|
||||
PathType = ClassifyPathType(highwayType),
|
||||
Name = tags.GetValueOrDefault("name"),
|
||||
IsWalkable = IsWalkableHighway(highwayType),
|
||||
Width = EstimatePathWidth(highwayType)
|
||||
Width = EstimatePathWidth(highwayType),
|
||||
IsPubliclyAccessible = IsPubliclyAccessibleWay(highwayType, tags)
|
||||
});
|
||||
}
|
||||
else if (tags.ContainsKey("leisure"))
|
||||
@@ -326,6 +327,143 @@ out skel qt;
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// P11: Strict public-access check for task placement. Returns true only
|
||||
/// when the way is unambiguously safe and legal for a foot player to
|
||||
/// stand on. Bandwidth's directive: "brutal check even at the cost of
|
||||
/// quality" - we'd rather refuse 80% of marginal candidates than place
|
||||
/// one task on a private driveway or a busy primary road. The `any
|
||||
/// pathway point` fallback in GetRandomReachablePosition still works for
|
||||
/// rural/forest scenarios where this filter rejects everything.
|
||||
///
|
||||
/// Hard rejects:
|
||||
/// - access tag in {private, no, customers, permit, forestry,
|
||||
/// agricultural, military, employees, delivery}
|
||||
/// - foot tag in {no, private, discouraged}
|
||||
/// - highway types known to be unsafe foot terrain (busy roads) or
|
||||
/// ambiguous (service ways often = parking lots / driveways unless
|
||||
/// explicitly tagged otherwise).
|
||||
/// </summary>
|
||||
private bool IsPubliclyAccessibleWay(string highway, Dictionary<string, string> tags)
|
||||
{
|
||||
// Hard-reject the highway types we never want a task on.
|
||||
switch (highway)
|
||||
{
|
||||
case "motorway":
|
||||
case "motorway_link":
|
||||
case "trunk":
|
||||
case "trunk_link":
|
||||
case "primary":
|
||||
case "primary_link":
|
||||
case "secondary":
|
||||
case "secondary_link":
|
||||
case "tertiary":
|
||||
case "tertiary_link":
|
||||
// Roads people drive fast on - even if foot is technically
|
||||
// allowed, putting a task target here invites tragedy.
|
||||
return false;
|
||||
case "construction":
|
||||
case "proposed":
|
||||
case "abandoned":
|
||||
case "razed":
|
||||
// Not even a real path right now.
|
||||
return false;
|
||||
case "raceway":
|
||||
case "bus_guideway":
|
||||
case "escape":
|
||||
// Specialized infrastructure, never appropriate.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Access tag: explicit private/restricted = reject.
|
||||
if (tags.TryGetValue("access", out var access))
|
||||
{
|
||||
access = access.ToLowerInvariant();
|
||||
switch (access)
|
||||
{
|
||||
case "private":
|
||||
case "no":
|
||||
case "customers":
|
||||
case "permit":
|
||||
case "forestry":
|
||||
case "agricultural":
|
||||
case "military":
|
||||
case "employees":
|
||||
case "delivery":
|
||||
case "destination":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// foot tag: explicit foot=no = reject regardless of highway type.
|
||||
if (tags.TryGetValue("foot", out var foot))
|
||||
{
|
||||
foot = foot.ToLowerInvariant();
|
||||
switch (foot)
|
||||
{
|
||||
case "no":
|
||||
case "private":
|
||||
case "discouraged":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// motor_vehicle / vehicle tags can flag a way as no-vehicles only,
|
||||
// which usually implies pedestrians are welcome - we don't reject
|
||||
// on those, just note them as positive evidence in the explicit-
|
||||
// approval branch below.
|
||||
|
||||
// Service ways: commonly driveways, parking lots, alleys. Reject by
|
||||
// default unless the access/foot/service tag explicitly opens it up.
|
||||
if (highway == "service")
|
||||
{
|
||||
// service=alley is generally walkable; service=driveway, parking_aisle,
|
||||
// emergency_access are not.
|
||||
if (tags.TryGetValue("service", out var serviceType))
|
||||
{
|
||||
serviceType = serviceType.ToLowerInvariant();
|
||||
if (serviceType == "alley") return true;
|
||||
// All other service subtypes default to "no" without explicit access=yes.
|
||||
if (access != null && (access == "yes" || access == "permissive" || access == "public"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
// No service subtag - too ambiguous, reject.
|
||||
// (Players can still walk on these IRL, but for "absolutely public"
|
||||
// we want clearer signal.)
|
||||
if (access == "yes" || access == "permissive" || access == "public")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// bridleway: technically horse paths; foot may or may not be allowed.
|
||||
// Require explicit foot=yes/designated to opt in.
|
||||
if (highway == "bridleway")
|
||||
{
|
||||
if (foot != null && (foot == "yes" || foot == "designated" || foot == "permissive"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Highway types we trust as inherently public foot terrain.
|
||||
switch (highway)
|
||||
{
|
||||
case "footway":
|
||||
case "path":
|
||||
case "pedestrian":
|
||||
case "steps":
|
||||
case "cycleway":
|
||||
case "residential":
|
||||
case "living_street":
|
||||
case "track":
|
||||
return true;
|
||||
}
|
||||
|
||||
// Everything else: reject by default. The brutal-mode point is to
|
||||
// not gamble on tags we don't recognize.
|
||||
return false;
|
||||
}
|
||||
|
||||
private double EstimatePathWidth(string highway)
|
||||
{
|
||||
@@ -369,49 +507,116 @@ out skel qt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a random reachable position suitable for placing a task or repair station
|
||||
/// Get a random reachable position suitable for placing a task or repair station.
|
||||
///
|
||||
/// P11 cascade (each tier falls through to the next on empty result):
|
||||
/// 1. Reachable AND on a publicly-accessible way (brutal mode preferred).
|
||||
/// 2. Any reachable position (covers the rare case where reachability
|
||||
/// analysis hit a way we don't deem "absolutely public" but is
|
||||
/// still pathway-connected).
|
||||
/// 3. Any walkable pathway point in radius (used in dense urban OR
|
||||
/// rural - the fallback Bandwidth specifically asked for: "When
|
||||
/// starting a game when there are almost no roads at all (at a
|
||||
/// field or in a forest) we can place tasks wherever").
|
||||
/// </summary>
|
||||
public Position? GetRandomReachablePosition(MapData mapData, Random random, double minDistFromCenter = 0)
|
||||
{
|
||||
// Tier 1: reachable AND on a publicly-accessible way.
|
||||
var publicWayPoints = CollectPubliclyAccessiblePoints(mapData);
|
||||
var candidates = mapData.ReachablePositions
|
||||
.Where(p => p.DistanceTo(mapData.Center) >= minDistFromCenter)
|
||||
.Where(p => publicWayPoints.Contains(p))
|
||||
.ToList();
|
||||
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
// Fallback to any pathway point
|
||||
// Tier 2: any reachable, ignoring the public-only filter.
|
||||
candidates = mapData.ReachablePositions
|
||||
.Where(p => p.DistanceTo(mapData.Center) >= minDistFromCenter)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
// Tier 3: any walkable pathway point in radius (rural fallback).
|
||||
candidates = mapData.Pathways
|
||||
.Where(p => p.IsWalkable)
|
||||
.SelectMany(p => p.Points)
|
||||
.Where(p => p.DistanceTo(mapData.Center) <= mapData.RadiusMeters)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return null;
|
||||
|
||||
|
||||
return candidates[random.Next(candidates.Count)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set of all positions that lie on a publicly-accessible pathway. Built
|
||||
/// once per call site (cheap; pathway count is bounded by the radius).
|
||||
/// HashSet of Position relies on Position.Equals/GetHashCode being
|
||||
/// content-based; if that ever changes, swap to a custom comparer.
|
||||
/// </summary>
|
||||
private HashSet<Position> CollectPubliclyAccessiblePoints(MapData mapData)
|
||||
{
|
||||
var set = new HashSet<Position>();
|
||||
foreach (var path in mapData.Pathways)
|
||||
{
|
||||
if (!path.IsPubliclyAccessible) continue;
|
||||
foreach (var p in path.Points)
|
||||
set.Add(p);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get multiple well-distributed reachable positions (e.g., for placing multiple tasks)
|
||||
/// Get multiple well-distributed reachable positions (e.g., for placing
|
||||
/// multiple tasks). Same P11 tier cascade as GetRandomReachablePosition:
|
||||
/// public-only first, fall through to reachable-any, then any walkable
|
||||
/// pathway point if even that's empty.
|
||||
/// </summary>
|
||||
public List<Position> GetDistributedReachablePositions(MapData mapData, int count, Random random, double minSpacing = 20)
|
||||
{
|
||||
var result = new List<Position>();
|
||||
var available = mapData.ReachablePositions.ToList();
|
||||
|
||||
|
||||
// Tier 1: reachable AND public.
|
||||
var publicWayPoints = CollectPubliclyAccessiblePoints(mapData);
|
||||
var available = mapData.ReachablePositions
|
||||
.Where(p => publicWayPoints.Contains(p))
|
||||
.ToList();
|
||||
|
||||
// Tier 2: any reachable.
|
||||
if (available.Count == 0)
|
||||
available = mapData.ReachablePositions.ToList();
|
||||
|
||||
// Tier 3: any walkable pathway point in radius. Logged so the server
|
||||
// op can see when the brutal filter exhausted itself - useful in
|
||||
// testing to know whether the area has decent OSM coverage.
|
||||
if (available.Count == 0)
|
||||
{
|
||||
available = mapData.Pathways
|
||||
.Where(p => p.IsWalkable)
|
||||
.SelectMany(p => p.Points)
|
||||
.Where(p => p.DistanceTo(mapData.Center) <= mapData.RadiusMeters)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (available.Count > 0)
|
||||
_logger.LogInformation("[Overpass] Task placement falling back to any-pathway-point - no publicly-accessible reachable geometry within radius {R}m of {Lat},{Lon}",
|
||||
mapData.RadiusMeters, mapData.Center.Lat, mapData.Center.Lon);
|
||||
}
|
||||
|
||||
// Shuffle available positions
|
||||
for (int i = available.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = random.Next(i + 1);
|
||||
(available[i], available[j]) = (available[j], available[i]);
|
||||
}
|
||||
|
||||
|
||||
foreach (var pos in available)
|
||||
{
|
||||
if (result.Count >= count) break;
|
||||
|
||||
|
||||
// Check minimum spacing from already selected positions
|
||||
bool tooClose = result.Any(r => r.DistanceTo(pos) < minSpacing);
|
||||
if (!tooClose)
|
||||
|
||||
Reference in New Issue
Block a user