Static helper for HorizonRP that reads the map's glass pane data from truenorth/vitres.json, indexes each pane by 512 inch ground cells, and answers whether a point is on a pane. Also provides a scene trace that restarts behind glass hits so shots and host hit checks pass through windows.
namespace HorizonRP;
/// <summary>
/// Les vitres de Truenorth (truenorth/vitres.json, écrit par outils/truenorth/vitres.py) : les
/// tuiles du monde n'ont qu'une collision, et une balle s'arrêtait sur un carreau comme sur un mur
/// (Julien, 30/10/2026). Le tir (SWB, HitScanBulletInfo) et les contrôles de l'hôte
/// (WeaponSystem.RequestHit…) demandent ici si le point touché est sur une vitre : la balle continue
/// alors derrière. Les joueurs, eux, butent toujours sur le carreau.
///
/// Chaque carreau est une face plane convexe ; ils sont rangés par cases de 512 pouces au sol, pour
/// n'en tester qu'une poignée par balle. Tout se lit une fois, à la première question ; la carte
/// absente (Union, éditeur sans Truenorth), rien n'est une vitre.
/// </summary>
public static class MapGlass
{
public const string DataPath = "truenorth/vitres.json";
const float CellSize = 512f;
/// <summary>Distance au plan du carreau en deçà de laquelle un point est dessus (pouces).</summary>
const float OnPane = 3f;
sealed record Pane( Vector3 Normal, float Distance, Vector3[] Points );
sealed class GlassFile
{
public float[][] vitres { get; set; }
}
static Dictionary<(int, int), List<Pane>> cells;
static Vector3 loadedOffset;
static Dictionary<(int, int), List<Pane>> Cells
{
get
{
var map = Game.ActiveScene?.GetAllComponents<TruenorthMap>().FirstOrDefault();
if ( map is null )
return null;
if ( cells is not null && loadedOffset == map.Offset )
return cells;
loadedOffset = map.Offset;
cells = new();
try
{
var file = Json.Deserialize<GlassFile>( FileSystem.Mounted.ReadAllText( DataPath ) );
foreach ( var row in file?.vitres ?? Array.Empty<float[]>() )
{
if ( row.Length < 13 )
continue;
var normal = new Vector3( row[0], row[1], row[2] );
var points = new Vector3[(row.Length - 4) / 3];
for ( var i = 0; i < points.Length; i++ )
points[i] = new Vector3( row[4 + i * 3], row[5 + i * 3], row[6 + i * 3] ) + map.Offset;
var pane = new Pane( normal, row[3] + Vector3.Dot( normal, map.Offset ), points );
var minX = Cell( points.Min( p => p.x ) );
var maxX = Cell( points.Max( p => p.x ) );
var minY = Cell( points.Min( p => p.y ) );
var maxY = Cell( points.Max( p => p.y ) );
for ( var x = minX; x <= maxX; x++ )
for ( var y = minY; y <= maxY; y++ )
{
if ( !cells.TryGetValue( (x, y), out var list ) )
cells[(x, y)] = list = new();
list.Add( pane );
}
}
}
catch ( Exception e )
{
Log.Warning( $"[Horizon] Vitres de Truenorth illisibles ({DataPath}) : {e.Message}" );
}
return cells;
}
}
static int Cell( float v ) => (int)MathF.Floor( v / CellSize );
/// <summary>Ce point (un impact) est-il sur une vitre de la map ?</summary>
public static bool Contains( Vector3 point )
{
var all = Cells;
if ( all is null || !all.TryGetValue( (Cell( point.x ), Cell( point.y )), out var panes ) )
return false;
foreach ( var pane in panes )
{
if ( MathF.Abs( Vector3.Dot( pane.Normal, point ) - pane.Distance ) > OnPane )
continue;
if ( Inside( pane, point ) )
return true;
}
return false;
}
/// <summary>
/// Le point est-il dans le polygone convexe (projeté sur son plan) ? Du même côté de chaque
/// arête, à une marge près : un impact au ras du cadre compte encore pour la vitre.
/// </summary>
static bool Inside( Pane pane, Vector3 point )
{
var sign = 0;
var p = pane.Points;
for ( var i = 0; i < p.Length; i++ )
{
var a = p[i];
var b = p[(i + 1) % p.Length];
var side = Vector3.Dot( Vector3.Cross( b - a, point - a ), pane.Normal );
var edge = (b - a).Length;
if ( MathF.Abs( side ) <= edge * 0.5f )
continue;
var s = MathF.Sign( side );
if ( sign == 0 )
sign = s;
else if ( s != sign )
return false;
}
return true;
}
/// <summary>
/// Un trait de <paramref name="from"/> à <paramref name="to"/> qui ne s'arrête pas aux vitres :
/// la trace est relancée derrière chaque carreau touché (quatre au plus). Pour les contrôles de
/// l'hôte, qui sinon refusaient toute touche à travers une fenêtre.
/// </summary>
public static SceneTraceResult ThroughGlass( SceneTrace trace, Vector3 from, Vector3 to )
{
var start = from;
var direction = (to - from).Normal;
SceneTraceResult result = default;
for ( var i = 0; i < 5; i++ )
{
result = trace.FromTo( start, to ).Run();
if ( !result.Hit || !Contains( result.HitPosition ) )
return result;
start = result.HitPosition + direction * 2f;
if ( Vector3.Dot( to - start, direction ) <= 0 )
return default;
}
return result;
}
}