swwmgz_m/zscript/utility/swwm_utility_map.zsc

622 lines
20 KiB
Text

// map interaction/info functions
enum EExitType
{
ET_Normal,
ET_Secret,
ET_EndGame,
ET_NewMap,
};
extend Class SWWMUtility
{
// how the fuck is this not available to ZScript?
// copied from P_PointOnLineSidePrecise()
deprecated("4.11", "Use Level.PointOnLineSide() instead") static clearscope int PointOnLineSide( Vector2 p, Line l )
{
if ( !l ) return 0;
return (((p.y-l.v1.p.y)*l.delta.x+(l.v1.p.x-p.x)*l.delta.y) > double.epsilon);
}
// haha another one
// copied from BoxOnLineSide()
deprecated("4.11", "Use Level.BoxOnLineSide() instead") static clearscope int BoxOnLineSide( double top, double bottom, double left, double right, Line l )
{
if ( !l ) return 0;
int p1, p2;
if ( l.delta.x == 0 )
{
// ST_VERTICAL:
p1 = (right < l.v1.p.x);
p2 = (left < l.v1.p.x);
if ( l.delta.y < 0 )
{
p1 ^= 1;
p2 ^= 1;
}
}
else if ( l.delta.y == 0 )
{
// ST_HORIZONTAL:
p1 = (top > l.v1.p.y);
p2 = (bottom > l.v1.p.y);
if ( l.delta.x < 0 )
{
p1 ^= 1;
p2 ^= 1;
}
}
else if ( (l.delta.x*l.delta.y) >= 0 )
{
// ST_POSITIVE:
p1 = PointOnLineSide((left,top),l);
p2 = PointOnLineSide((right,bottom),l);
}
else
{
// ST_NEGATIVE:
p1 = PointOnLineSide((right,top),l);
p2 = PointOnLineSide((left,bottom),l);
}
return (p1==p2)?p1:-1;
}
// wrapper
deprecated("4.11", "Use Level.ActorOnLineSide() instead") static clearscope int ActorOnLineSide( Actor a, Line l )
{
double box[4];
box[0] = a.pos.y+a.radius;
box[1] = a.pos.y-a.radius;
box[2] = a.pos.x-a.radius;
box[3] = a.pos.x+a.radius;
return BoxOnLineSide(box[0],box[1],box[2],box[3],l);
}
// Thanks to ZZYZX and Nash
static play void SetToSlopeSpecific( Actor a, double dang, SecPlane plane, bool flipnorm )
{
Vector3 fnormal;
if ( flipnorm ) fnormal = -plane.Normal;
else fnormal = plane.Normal;
vector2 fnormalp1 = ((fnormal.x != 0) || (fnormal.y != 0))?(fnormal.x,fnormal.y).Unit():(0,0);
vector2 fnormalp2 = ((fnormal.x,fnormal.y).Length(),fnormal.z);
double fang = atan2(fnormalp1.y,fnormalp1.x); // floor angle (not pitch!)
double fpitch = atan2(fnormalp2.x,fnormalp2.y); // floor pitch
double ddiff1 = sin(fang-dang);
double ddiff2 = cos(fang-dang);
a.pitch = fpitch*ddiff2;
a.roll = -fpitch*ddiff1;
a.angle = dang;
}
static play void SetToSlope( Actor a, double dang, bool ceil = false )
{
Sector sect;
SecPlane plane;
Vector3 fnormal;
bool flipnorm;
if ( ceil )
{
sect = a.CeilingSector;
plane = sect.ceilingplane;
flipnorm = true;
fnormal = -sect.ceilingplane.Normal;
}
else
{
sect = a.FloorSector;
plane = sect.floorplane;
flipnorm = false;
fnormal = sect.floorplane.Normal;
}
// find closest 3d floor for its normal
F3DFloor ff;
for ( int i=0; i<sect.Get3DFloorCount(); i++ )
{
if ( !(sect.Get3DFloor(i).flags&F3DFloor.FF_SOLID) ) continue;
if ( !ceil && !(sect.Get3DFloor(i).top.ZAtPoint(a.pos.xy) ~== a.floorz) ) continue;
else if ( ceil && !(sect.Get3DFloor(i).top.ZAtPoint(a.pos.xy) ~== a.ceilingz) ) continue;
ff = sect.Get3DFloor(i);
break;
}
if ( ff )
{
if ( ceil )
{
plane = ff.bottom;
flipnorm = false;
fnormal = ff.bottom.Normal;
}
else
{
plane = ff.top;
flipnorm = true;
fnormal = -ff.top.Normal;
}
}
SetToSlopeSpecific(a,dang,plane,flipnorm);
}
static clearscope int GetLineLock( Line l )
{
int locknum = l.locknumber;
if ( !locknum )
{
// check the special
switch ( l.special )
{
case FS_Execute:
locknum = l.Args[2];
break;
case Door_LockedRaise:
case Door_Animated:
locknum = l.Args[3];
break;
case ACS_LockedExecute:
case ACS_LockedExecuteDoor:
case Generic_Door:
locknum = l.Args[4];
break;
}
}
return locknum;
}
// return if a line is an exit, and additionally the type of exit
static clearscope bool, int IsExitLine( Line l )
{
if ( l.special == Exit_Secret )
return true, ET_Secret;
if ( l.special == Exit_Normal )
return true, ET_Normal;
if ( l.special == Teleport_EndGame )
return true, ET_EndGame;
if ( l.special == Teleport_NewMap )
return true, ET_NewMap;
// E1M8 compat
if ( (l.special == ACS_Execute) && (l.Args[0] == -Int('E1M8_KNOCKOUT')) )
return true, ET_Normal;
// spooktober™
if ( ((l.special == ACS_Execute) || (l.special == ACS_ExecuteAlways)) && (l.Args[0] == -Int('MapFadeOut')) )
{
if ( level.levelnum == 1 )
{
let lv = levelinfo.FindLevelByNum(l.Args[2]);
if ( lv && lv.mapname.Left(6) ~== "SECRET" )
return true, ET_Secret;
else return true, ET_NewMap;
}
return true, ET_Normal;
}
return false, ET_Normal;
}
static clearscope bool IsTeleportLine( Line l, bool all = false )
{
// must be two-sided and crossable
if ( !l.sidedef[1] || !(l.Activation&(SPAC_Cross|SPAC_MCross|SPAC_PCross|SPAC_AnyCross)) ) return false;
// filter lines that aren't player-activated (unless checking all)
if ( !all && !(l.Activation&SPAC_PlayerActivate) ) return false;
// typical teleports
if ( (l.special == Teleport) || (l.special == Teleport_NoStop) )
return true;
// if checking all, also include sneaky teleports
if ( all && ((l.special == Teleport_Line) || (l.special == Teleport_NoFog)) )
return true;
// exits are included too
if ( IsExitLine(l) )
return true;
return false;
}
static clearscope bool IsValidLockNum( int l )
{
if ( (l < 1) || (l > 255) ) return true;
return SWWMCachedLockInfo.IsValidLock(l);
}
static clearscope Color GetLockColor( int l )
{
return SWWMCachedLockInfo.GetLockColor(l);
}
static clearscope bool IsDoorSector( Sector s, int part )
{
// super-easy mode: check for boss special sectors
if ( (level.mapname ~== "E1M8") || (level.mapname ~== "E2M8") || (level.mapname ~== "E3M8")
|| (level.mapname ~== "E4M6") || (level.mapname ~== "E4M8") || (level.mapname ~== "E5M8")
|| (level.mapname ~== "MAP07") )
{
let si = level.CreateSectorTagIterator(666);
int idx;
while ( (idx = si.Next()) != -1 )
if ( level.Sectors[idx] == s )
return true;
if ( level.mapname ~== "MAP07" )
{
let si2 = level.CreateSectorTagIterator(667);
while ( (idx = si.Next()) != -1 )
if ( level.Sectors[idx] == s )
return true;
}
}
// moderate: see if it's a busted crusher, we need to be able to break those in case they cause a softlock
let ti = ThinkerIterator.Create("SWWMCrusherBroken",Thinker.STAT_USER);
SWWMCrusherBroken cb;
while ( cb = SWWMCrusherBroken(ti.Next()) )
{
if ( (part == 0) && (cb.fsec == s) ) return true;
if ( (part == 1) && (cb.csec == s) ) return true;
}
// hard mode: look for all lines/actors with movement specials referencing us
foreach ( l:level.Lines )
{
if ( !l.special ) continue;
if ( (part && (l.special >= 10) && (l.special <= 13))
|| (!part && (l.special >= 20) && (l.special <= 25))
|| (!part && (l.special == 28))
|| ((l.special >= 29) && (l.special <= 30))
|| (!part && (l.special >= 35) && (l.special <= 37))
|| (part && (l.special >= 40) && (l.special <= 45))
|| (!part && (l.special == 46))
|| (part && (l.special == 47))
|| (!part && (l.special >= 60) && (l.special <= 68))
|| (part && (l.special == 69))
|| ((l.special >= 94) && (l.special <= 96))
|| (part && (l.special == 97))
|| (!part && (l.special == 99))
|| (part && (l.special == 104))
|| (part && (l.special >= 105) && (l.special <= 106))
|| (part && (l.special >= 168) && (l.special <= 169))
|| (!part && (l.special == 172))
|| (part && (l.special >= 192) && (l.special <= 199))
|| (!part && (l.special == 200))
|| (part && (l.special >= 201) && (l.special <= 202))
|| (!part && (l.special == 203))
|| (part && (l.special == 205))
|| (!part && (l.special >= 206) && (l.special <= 207))
|| (!part && (l.special == 228))
|| (!part && (l.special >= 230) && (l.special <= 231))
|| (!part && (l.special >= 238) && (l.special <= 242))
|| ((l.special >= 245) && (l.special <= 247))
|| (part && (l.special == 249))
|| (!part && (l.special >= 250) && (l.special <= 251))
|| (part && (l.special >= 251) && (l.special <= 255))
|| (!part && (l.special >= 256) && (l.special <= 261))
|| (part && (l.special >= 262) && (l.special <= 269))
|| (!part && (l.special == 275))
|| (part && (l.special == 276))
|| (!part && (l.special == 279))
|| (part && (l.special == 280)) )
{
let si = level.CreateSectorTagIterator(l.Args[0],l);
int idx;
while ( (idx = si.Next()) != -1 )
if ( level.Sectors[idx] == s )
return true;
}
}
ti = ThinkerIterator.Create("Actor");
Actor a;
while ( a = Actor(ti.Next()) )
{
if ( !a.special || !a.Args[0] ) continue;
if ( (part && (a.special >= 10) && (a.special <= 13))
|| (!part && (a.special >= 20) && (a.special <= 25))
|| (!part && (a.special == 28))
|| ((a.special >= 29) && (a.special <= 30))
|| (!part && (a.special >= 35) && (a.special <= 37))
|| (part && (a.special >= 40) && (a.special <= 45))
|| (!part && (a.special == 46))
|| (part && (a.special == 47))
|| (!part && (a.special >= 60) && (a.special <= 68))
|| (part && (a.special == 69))
|| ((a.special >= 94) && (a.special <= 96))
|| (part && (a.special == 97))
|| (!part && (a.special == 99))
|| (part && (a.special == 104))
|| (part && (a.special >= 105) && (a.special <= 106))
|| (part && (a.special >= 168) && (a.special <= 169))
|| (!part && (a.special == 172))
|| (part && (a.special >= 192) && (a.special <= 199))
|| (!part && (a.special == 200))
|| (part && (a.special >= 201) && (a.special <= 202))
|| (!part && (a.special == 203))
|| (part && (a.special == 205))
|| (!part && (a.special >= 206) && (a.special <= 207))
|| (!part && (a.special == 228))
|| (!part && (a.special >= 230) && (a.special <= 231))
|| (!part && (a.special >= 238) && (a.special <= 242))
|| ((a.special >= 245) && (a.special <= 247))
|| (part && (a.special == 249))
|| (!part && (a.special >= 250) && (a.special <= 251))
|| (part && (a.special >= 251) && (a.special <= 255))
|| (!part && (a.special >= 256) && (a.special <= 261))
|| (part && (a.special >= 262) && (a.special <= 269))
|| (!part && (a.special == 275))
|| (part && (a.special == 276))
|| (!part && (a.special == 279))
|| (part && (a.special == 280)) )
{
let si = level.CreateSectorTagIterator(a.Args[0]);
int idx;
while ( (idx = si.Next()) != -1 )
if ( level.Sectors[idx] == s )
return true;
}
}
return false;
}
// the stupidest thing ever, it's called BlockingLine but it's not always blocking us
static play bool BlockingLineIsBlocking( Actor a, int blockflags = Line.ML_BLOCKEVERYTHING, Line testline = null )
{
Line l = testline?testline:a.BlockingLine;
// not blocked
if ( !l ) return false;
// one-sided always blocking
if ( !l.sidedef[1] ) return true;
// same for block everything lines
if ( l.flags&blockflags ) return true;
// lower and upper bounds hit?
double afloor = l.frontsector.floorplane.ZAtPoint(a.pos.xy),
bfloor = l.backsector.floorplane.ZAtPoint(a.pos.xy),
aceil = l.frontsector.ceilingplane.ZAtPoint(a.pos.xy),
bceil = l.backsector.ceilingplane.ZAtPoint(a.pos.xy);
if ( (min(a.pos.z+a.height,a.ceilingz) > min(aceil,bceil)) || (max(a.pos.z,a.floorz) < max(afloor,bfloor)) )
return true;
// solid 3d floor bounds hit?
for ( int i=0; i<l.frontsector.Get3DFloorCount(); i++ )
{
F3DFloor ff = l.frontsector.Get3DFloor(i);
if ( !(ff.flags&(F3DFloor.FF_EXISTS|F3DFloor.FF_SOLID)) ) continue;
double floor = ff.top.ZAtPoint(a.pos.xy);
double ceil = ff.bottom.ZAtPoint(a.pos.xy);
if ( (a.pos.z+a.height > ceil) && (a.pos.z < floor) )
return true;
}
for ( int i=0; i<l.backsector.Get3DFloorCount(); i++ )
{
F3DFloor ff = l.backsector.Get3DFloor(i);
if ( !(ff.flags&(F3DFloor.FF_EXISTS|F3DFloor.FF_SOLID)) ) continue;
double floor = ff.top.ZAtPoint(a.pos.xy);
double ceil = ff.bottom.ZAtPoint(a.pos.xy);
if ( (a.pos.z+a.height > ceil) && (a.pos.z < floor) )
return true;
}
return false;
}
static play Vector3 UseLinePos( Line l )
{
Vector3 al, ah, bl, bh;
if ( !l.sidedef[1] )
{
// just the whole line
al = (l.v1.p,l.frontsector.floorplane.ZatPoint(l.v1.p));
ah = (l.v1.p,l.frontsector.ceilingplane.ZatPoint(l.v1.p));
bl = (l.v2.p,l.frontsector.floorplane.ZatPoint(l.v2.p));
bh = (l.v2.p,l.frontsector.ceilingplane.ZatPoint(l.v2.p));
return (al+ah+bl+bh)*.25;
}
SecPlane highestfloor, lowestfloor, lowestceiling, highestceiling;
if ( (l.frontsector.floorplane.ZatPoint(l.v1.p) > l.backsector.floorplane.ZatPoint(l.v1.p))
&& (l.frontsector.floorplane.ZatPoint(l.v2.p) > l.backsector.floorplane.ZatPoint(l.v2.p)) )
{
highestfloor = l.frontsector.floorplane;
lowestfloor = l.backsector.floorplane;
}
else
{
highestfloor = l.backsector.floorplane;
lowestfloor = l.frontsector.floorplane;
}
if ( (l.frontsector.ceilingplane.ZatPoint(l.v1.p) < l.backsector.ceilingplane.ZatPoint(l.v1.p))
&& (l.frontsector.ceilingplane.ZatPoint(l.v2.p) < l.backsector.ceilingplane.ZatPoint(l.v2.p)) )
{
lowestceiling = l.frontsector.ceilingplane;
highestceiling = l.backsector.ceilingplane;
}
else
{
lowestceiling = l.backsector.ceilingplane;
highestceiling = l.frontsector.ceilingplane;
}
// try to guess what the part that triggers this is
if ( l.Activation&SPAC_Cross )
{
// pick the "intersection"
al = (l.v1.p,highestfloor.ZatPoint(l.v1.p));
ah = (l.v1.p,lowestceiling.ZatPoint(l.v1.p));
bl = (l.v2.p,highestfloor.ZatPoint(l.v2.p));
bh = (l.v2.p,lowestceiling.ZatPoint(l.v2.p));
return (al+ah+bl+bh)*.25;
}
// check if lower part available
al = (l.v1.p,lowestfloor.ZatPoint(l.v1.p));
ah = (l.v1.p,highestfloor.ZatPoint(l.v1.p));
bl = (l.v2.p,lowestfloor.ZatPoint(l.v2.p));
bh = (l.v2.p,highestfloor.ZatPoint(l.v2.p));
if ( ((al-ah).length() > 0) && ((bl-bh).length() > 0) )
return (al+ah+bl+bh)*.25;
// check if upper part available
al = (l.v1.p,lowestceiling.ZatPoint(l.v1.p));
ah = (l.v1.p,highestceiling.ZatPoint(l.v1.p));
bl = (l.v2.p,lowestceiling.ZatPoint(l.v2.p));
bh = (l.v2.p,highestceiling.ZatPoint(l.v2.p));
if ( ((al-ah).length() > 0) && ((bl-bh).length() > 0) )
return (al+ah+bl+bh)*.25;
// check for 3d floors
bool floorfound = false;
Vector3 fal, fah, fbl, fbh;
for ( int i=0; i<l.backsector.Get3DFloorCount(); i++ )
{
let ff = l.backsector.Get3DFloor(i);
fal = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
fah = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
fbl = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
fbh = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
// skip if higher, we'll go with the lowest 3d floor (may not be right, but whatever)
if ( floorfound && (fah.z > ah.z) && (fbh.z > bh.z) && (fal.z > al.z) && (fbl.z > bl.z) ) continue;
al = fal;
ah = fah;
bl = fbl;
bh = fbh;
floorfound = true;
}
if ( floorfound ) return (al+ah+bl+bh)*.25;
for ( int i=0; i<l.frontsector.Get3DFloorCount(); i++ )
{
let ff = l.frontsector.Get3DFloor(i);
fal = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
fah = (l.v1.p,ff.model.floorplane.ZAtPoint(l.v1.p));
fbl = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
fbh = (l.v2.p,ff.model.ceilingplane.ZAtPoint(l.v2.p));
// skip if higher, we'll go with the lowest 3d floor (may not be right, but whatever)
if ( floorfound && (fah.z > ah.z) && (fbh.z > bh.z) && (fal.z > al.z) && (fbl.z > bl.z) ) continue;
al = fal;
ah = fah;
bl = fbl;
bh = fbh;
floorfound = true;
}
if ( floorfound ) return (al+ah+bl+bh)*.25;
// check for midtex
if ( !l.sidedef[0].GetTexture(1).IsNull() )
{
double ofs = l.sidedef[0].GetTextureYOffset(1);
Vector2 siz = TexMan.GetScaledSize(l.sidedef[0].GetTexture(1));
Vector2 tofs = TexMan.GetScaledOffset(l.sidedef[0].GetTexture(1));
ofs += tofs.y;
ofs *= l.sidedef[0].GetTextureYScale(1);
siz.y *= l.sidedef[0].GetTextureYScale(1);
if ( l.flags&Line.ML_DONTPEGBOTTOM )
{
al = (l.v1.p,highestfloor.ZAtPoint(l.v1.p)+ofs);
bl = (l.v2.p,highestfloor.ZAtPoint(l.v2.p)+ofs);
ah = al+(0,0,siz.y);
bh = bl+(0,0,siz.y);
}
else
{
ah = (l.v1.p,lowestceiling.ZAtPoint(l.v1.p)+ofs);
bh = (l.v2.p,lowestceiling.ZAtPoint(l.v2.p)+ofs);
al = ah-(0,0,siz.y);
bl = bh-(0,0,siz.y);
}
return (al+ah+bl+bh)*.25;
}
if ( !l.sidedef[1].GetTexture(1).IsNull() )
{
double ofs = l.sidedef[1].GetTextureYOffset(1);
Vector2 siz = TexMan.GetScaledSize(l.sidedef[1].GetTexture(1));
Vector2 tofs = TexMan.GetScaledOffset(l.sidedef[1].GetTexture(1));
ofs += tofs.y;
ofs *= l.sidedef[1].GetTextureYScale(1);
siz.y *= l.sidedef[1].GetTextureYScale(1);
if ( l.flags&Line.ML_DONTPEGBOTTOM )
{
al = (l.v1.p,highestfloor.ZAtPoint(l.v1.p)+ofs);
bl = (l.v2.p,highestfloor.ZAtPoint(l.v2.p)+ofs);
ah = al+(0,0,siz.y);
bh = bl+(0,0,siz.y);
}
else
{
ah = (l.v1.p,lowestceiling.ZAtPoint(l.v1.p)+ofs);
bh = (l.v2.p,lowestceiling.ZAtPoint(l.v2.p)+ofs);
al = ah-(0,0,siz.y);
bl = bh-(0,0,siz.y);
}
return (al+ah+bl+bh)*.25;
}
// just use the intersection
al = (l.v1.p,highestfloor.ZatPoint(l.v1.p));
ah = (l.v1.p,lowestceiling.ZatPoint(l.v1.p));
bl = (l.v2.p,highestfloor.ZatPoint(l.v2.p));
bh = (l.v2.p,lowestceiling.ZatPoint(l.v2.p));
return (al+ah+bl+bh)*.25;
}
static bool, TextureID DefaceTexture( TextureID checkme )
{
String tn = TexMan.GetName(checkme);
// special case: alt texture names in Doom 2 In Spain Only
if ( (tn ~== "MARBFAC2") || (tn ~== "SP_MAR01") )
return true, TexMan.CheckForTexture("defaced_MARBFAC2");
if ( (tn ~== "MARBFAC3") || (tn ~== "SP_MAR02"))
return true, TexMan.CheckForTexture("defaced_MARBFAC3");
if ( (tn ~== "MARBFAC4") || (tn ~== "SP_MAR03") )
return true, TexMan.CheckForTexture("defaced_MARBFAC4");
if ( (tn ~== "MARBFACE") || (tn ~== "SP_MAR04") )
return true, TexMan.CheckForTexture("defaced_MARBFACE");
if ( (tn ~== "ZZWOLF2") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF2");
if ( (tn ~== "ZZWOLF3") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF3");
if ( (tn ~== "ZZWOLF4") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF4");
if ( (tn ~== "ZZWOLF6") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF6");
if ( (tn ~== "ZZWOLF7") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF7");
if ( (tn ~== "ZZWOLF12") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF12");
if ( (tn ~== "ZZWOLF13") )
return true, TexMan.CheckForTexture("defaced_ZZWOLF13");
return false, checkme;
}
// iterate through polyobjects and see if this line is part of one (returning which, if any)
static bool IsPolyLine( Line l, out swwm_PolyobjectHandle o )
{
let pi = swwm_PolyobjectIterator.Create();
swwm_PolyobjectHandle p;
while ( p = pi.Next() )
{
if ( p.Lines.Find(l) >= p.Lines.Size() ) continue;
o = p;
return true;
}
o = null;
return false;
}
// checks if the specified world coordinate is inside the polyobject
// this check is very naive but it should handle most "normal" shapes
// (yeah, sorry if you somehow want to play this mod with lilith.pk3)
static bool PointInPolyobj( Vector2 p, swwm_PolyobjectHandle o )
{
// first pass, find which vertex out of all lines is closest
Vertex v = o.StartLine.v1;
double dist = (v.p-p).length();
foreach ( l:o.Lines )
{
double dist2 = (l.v1.p-p).length();
if ( dist2 < dist )
{
v = l.v1;
dist = dist2;
}
dist2 = (l.v2.p-p).length();
if ( dist2 < dist )
{
v = l.v2;
dist = dist2;
}
}
// second pass, find which two lines share that vertex
// (in theory there should only be two)
Line a = null, b = null;
foreach ( l:o.Lines )
{
if ( (l.v1 == v) || (l.v2 == v) )
{
if ( !a ) a = l;
else if ( !b ) b = l;
else break;
}
}
// is the point behind both lines?
return (PointOnLineSide(p,a) && PointOnLineSide(p,b));
}
}