RespawnBsp
Cells & Portals
#84
Replies: 19 comments 10 replies
-
anyway, I was going to checkout out portals & cells Footnotes
|
Beta Was this translation helpful? Give feedback.
-
>>> import bsp_tool
>>> bsp_tool.load_bsp("E:/Mod/TitanfallOnline/maps/mp_box.bsp")
<RespawnBsp 'mp_box.bsp' respawn.titanfall (rBSP version 29)>
>>> box = _
>>> len(box.CELLS)
26
>>> len(box.PORTALS)
225
>>> indices = [(c.first_portal, c.num_portals) for c in box.CELLS]
>>> all_indices = {i for s, e in indices for i in range(s, s + e)}
>>> all_indices == {*range(225)}
True
>>> box.CELLS[25]
Cell(num_portals=0, first_portal=225, flags=<CellSkyFlags.UNKNOWN_3|UNKNOWN_2: 3>, leaf_water_data=-1)
>>> len([p for p in box.PORTALS if p.cell == 25])
0
|
Beta Was this translation helpful? Give feedback.
-
Let's see what we're working with here: LumpsCELL_BSP_NODES = 0x006A
CELLS = 0x006B
PORTALS = 0x006C
PORTAL_VERTICES = 0x006D
PORTAL_EDGES = 0x006E
PORTAL_VERTEX_EDGES = 0x006F
PORTAL_VERTEX_REFERENCES = 0x0070
PORTAL_EDGE_REFERENCES = 0x0071
PORTAL_EDGE_INTERSECT_AT_EDGE = 0x0072
PORTAL_EDGE_INTERSECT_AT_VERTEX = 0x0073
PORTAL_EDGE_INTERSECT_HEADER = 0x0074
CELL_AABB_NODES = 0x0077 Structs# {"LUMP_NAME": {version: LumpClass}}
BASIC_LUMP_CLASSES = {"PORTAL_EDGE_REFERENCES": {0: shared.UnsignedShorts},
"PORTAL_VERTEX_REFERENCES": {0: shared.UnsignedShorts}}
LUMP_CLASSES = {"CELLS": {0: Cell},
"CELL_AABB_NODES": {0: CellAABBNode},
"CELL_BSP_NODES": {0: CellBSPNode},
"PORTALS": {0: Portal},
"PORTAL_EDGES": {0: quake.Edge},
"PORTAL_EDGE_INTERSECT_AT_EDGE": {0: PortalIndexSet},
"PORTAL_EDGE_INTERSECT_AT_VERTEX": {0: PortalIndexSet},
"PORTAL_EDGE_INTERSECT_HEADER": {0: PortalEdgeIntersectHeader},
"PORTAL_VERTEX_EDGES": {0: PortalIndexSet}, # unsure
"PORTAL_VERTICES": {0: quake.Vertex}} #include <stdint.h>
/* NOTE: most member names from from rexx' structs & haven't been confirmed */
typedef uint16_t PortalEdgeReference;
typedef uint16_t PortalVertexReference;
typedef uint16_t[2] Edge;
typedef struct { float x, y, z; } Vector3;
typedef Vector3 Vertex;
struct Cell {
int16_t num_portals;
int16_t first_portal;
int16_t flags;
int16_t leaf_water_data;
};
struct CellAABBNode { /* "Extreme SIMD" aligned */
Vector3 origin;
uint8_t num_children;
uint8_t num_obj_refs;
uint16_t total_obj_refs;
Vector3 extents;
uint16_t first_child;
uint16_t first_obj_ref;
};
struct CellBSPNode { int32_t plane, child; };
struct Portal {
uint8_t is_reversed;
uint8_t type;
uint8_t num_edges;
uint8_t padding;
int16_t first_reference;
int16_t cell;
int32_t plane;
};
struct PortalIndexSet {
int16_t index[8]; /* up to 8 indices; -1 for none */
};
struct PortalEdgeIntersectHeader { uint32_t start, count; }; |
Beta Was this translation helpful? Give feedback.
-
Trying to pull out the windings from Assuming >>> box.PORTALS[0]
<Portal (is_reversed: 0, type: 0, num_edges: 4, padding: 0, first_reference: 0, cell: 1, plane: 0)>
>>> box.PORTAL_EDGE_REFERENCES[0:4] # first_ref : first_ref + num_edges
[0, 2, 4, 6]
>>> box.PLANES[0]
Plane(normal=vec3(0.0, -1.0, 0.0), distance=15936.0) # y == -15936
>>> box.PORTAL_EDGES[0]
[4, 5]
>>> box.PORTAL_VERTEX_REFERENCES[4:5 + 1]
[5, 6]
>>> box.PORTAL_VERTICES[5:6 + 1]
[<Vertex (x: 15904.0, y: -15936.0, z: -8304.0)>, <Vertex (x: 16928.0, y: -15936.0, z: -8304.0)>] # edge 1 |
Beta Was this translation helpful? Give feedback.
-
>>> def f(bsp, portal_index):
... portal = bsp.PORTALS[portal_index]
... for eri in range(portal.first_reference, portal.first_reference + portal.num_edges):
... e = bsp.PORTAL_EDGES[bsp.PORTAL_EDGE_REFERENCES[eri]]
... for vri in e:
... yield bsp.PORTAL_VERTICES[bsp.PORTAL_VERTEX_REFERENCES[vri]]
...
>>> list(f(box, 0))
[<Vertex (x: 15904.0, y: -15936.0, z: -8304.0)>, <Vertex (x: 16928.0, y: -15936.0, z: -8304.0)>,
<Vertex (x: 16928.0, y: -15936.0, z: 14384.0)>, <Vertex (x: 15904.0, y: -15936.0, z: 15408.0)>,
<Vertex (x: -7440.0, y: 16896.0, z: -8304.0)>, <Vertex (x: 16928.0, y: -15936.0, z: 14384.0)>,
<Vertex (x: -15872.0, y: -15936.0, z: 14384.0)>, <Vertex (x: 15904.0, y: -15936.0, z: 15408.0)>]
>>> p0vs = _
>>> len(p0vs)
8
>>> len(set(p0vs))
6
>>> [v for v in p0vs if v.y != -15936]
[<Vertex (x: -7440.0, y: 16896.0, z: -8304.0)>]
>>> p0vs.index(_[0])
4 not great
|
Beta Was this translation helpful? Give feedback.
-
>>> def g(bsp, portal_index):
... portal = bsp.PORTALS[portal_index]
... for eri in range(portal.first_reference, portal.first_reference + portal.num_edges):
... e = bsp.PORTAL_EDGES[bsp.PORTAL_EDGE_REFERENCES[eri]]
... for vri in e:
... yield vri
...
>>> list(g(box, 0))
[4, 5, 6, 3, 81, 6, 11, 9] Cells & Portals don't use all of the axial / non-brush planes in >>> {p.plane for p in box.PORTALS}
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
>>> {cbn.plane for cbn in box.CELL_BSP_NODES}
{0, 1, 2, 3, 4, 6, 7, 8, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 33, 34, 35, -1} # -1? |
Beta Was this translation helpful? Give feedback.
-
>>> {print(f"{L:<16} {getattr(box, L)!r}") for L, h in box.headers.items() if h.length != 0 and L.startswith("CELL")}
CELL_BSP_NODES <BspLump(51 CellBSPNode) at 0x000001D030D45580>
CELLS <BspLump(26 Cell) at 0x000001D030D45610>
CELL_AABB_NODES <BspLump(206 CellAABBNode) at 0x000001D030D45B20>
{None} 51 is almost 2x 26, but not quite >>> max({can.first_child + can.num_children for can in box.CELL_AABB_NODES})
206 # len(box.CELL_AABB_NODES); cannot index CellBspNodes or Cells, must be indexing itself (no child type flag afaik)
I could put an maybe I'll get lucky and there's a 100% on-plane Also, I'm fairly confident not going through |
Beta Was this translation helpful? Give feedback.
-
>>> box.PORTALS[0]
<Portal (is_reversed: 0, type: 0, num_edges: 4, padding: 0, first_reference: 0, cell: 1, plane: 0)>
>>> box.PORTAL_EDGES[0:4]
[[4, 5], [8, 2], [6, 3], [3, 7]]
>>> [box.PORTAL_EDGES[i] for i in box.PORTAL_EDGE_REFERENCES[0:4]]
[[4, 5], [6, 3], [81, 6], [11, 9]] This doesn't read like a Source face winding |
Beta Was this translation helpful? Give feedback.
-
much more reasonable: >>> lobby_02 = bsp_tool.load_bsp("E:/Mod/TitanfallOnline/maps/mp_lobby_02.bsp")
>>> {print(f"{L:<32} {getattr(lobby_02, L)!r}") for L, h in lobby_02.headers.items() if h.length != 0 and (L.startswith("CELL") or L.startswith("PORTAL"))}
CELL_BSP_NODES <BspLump(1 CellBSPNode) at 0x00000251871D6430>
CELLS <BspLump(1 Cell) at 0x00000251871D6490>
PORTALS <BspLump(6 Portal) at 0x00000251871D64F0>
PORTAL_VERTICES <BspLump(9 Vertex) at 0x00000251871D6550>
PORTAL_EDGES <BspLump(12 Edge) at 0x00000251871D65B0>
PORTAL_VERTEX_EDGES <BspLump(9 PortalIndexSet) at 0x00000251871D6610>
PORTAL_VERTEX_REFERENCES <BasicBspLump(24 UnsignedShorts) at 0x00000251871D6670>
PORTAL_EDGE_REFERENCES <BasicBspLump(24 UnsignedShorts) at 0x00000251871D66D0>
PORTAL_EDGE_INTERSECT_AT_EDGE <BspLump(12 PortalIndexSet) at 0x00000251871D6730>
PORTAL_EDGE_INTERSECT_AT_VERTEX <BspLump(12 PortalIndexSet) at 0x00000251871D6790>
PORTAL_EDGE_INTERSECT_HEADER <BspLump(12 PortalEdgeIntersectHeader) at 0x00000251871D67F0>
CELL_AABB_NODES <BspLump(24 CellAABBNode) at 0x00000251871D6850> |
Beta Was this translation helpful? Give feedback.
-
wait... I should probably check: >>> r1lobby = bsp_tool.load_bsp("E:/Mod/Titanfall/maps/mp_lobby.bsp")
>>> {print(f"{L:<32} {getattr(r1lobby, L)!r}") for L, h in r1lobby.headers.items() if h.length != 0 and (L.startswith("CELL") or L.startswith("PORTAL"))}
CELL_BSP_NODES <BspLump(1 CellBSPNode) at 0x00000251874176D0>
CELLS <BspLump(1 Cell) at 0x0000025187417730>
PORTAL_VERTICES <BspLump(1 Vertex) at 0x0000025187417790>
PORTAL_VERTEX_EDGES <BspLump(1 PortalIndexSet) at 0x0000025187417820>
CELL_AABB_NODES <BspLump(1 CellAABBNode) at 0x0000025187417A60>
{None}
>>> r2lobby = bsp_tool.load_bsp("E:/Mod/Titanfall2/maps/mp_lobby.bsp")
>>> {print(f"{L:<32} {getattr(r2lobby, L)!r}") for L, h in r2lobby.headers.items() if h.length != 0 and (L.startswith("CELL") or L.startswith("PORTAL"))}
CELL_BSP_NODES <BspLump(1 CellBSPNode) at 0x000002518742ABE0>
CELLS <BspLump(1 Cell) at 0x000002518742AC40>
PORTALS <BspLump(6 Portal) at 0x000002518742ACA0>
PORTAL_VERTICES <BspLump(9 Vertex) at 0x000002518742AD00>
PORTAL_EDGES <BspLump(12 Edge) at 0x000002518742AD60>
PORTAL_VERTEX_EDGES <BspLump(9 PortalIndexSet) at 0x000002518742ADC0>
PORTAL_VERTEX_REFERENCES <BasicBspLump(24 UnsignedShorts) at 0x000002518742AE20>
PORTAL_EDGE_REFERENCES <BasicBspLump(24 UnsignedShorts) at 0x000002518742AE80>
PORTAL_EDGE_INTERSECT_AT_EDGE <BspLump(12 PortalIndexSet) at 0x000002518742AEE0>
PORTAL_EDGE_INTERSECT_AT_VERTEX <BspLump(12 PortalIndexSet) at 0x000002518742AF40>
PORTAL_EDGE_INTERSECT_HEADER <BspLump(12 PortalEdgeIntersectHeader) at 0x000002518742AFA0>
CELL_AABB_NODES <BspLump(3 CellAABBNode) at 0x0000025187434040>
{None} interesting r2 lobby has no skybox brushes, is it one cell open to the sky w/ portals on each side of the bounds? >>> r2lobby.CELLS[0]
Cell(num_portals=6, first_portal=0, flags=<CellSkyFlags.UNKNOWN_4|UNKNOWN_2: 5>, leaf_water_data=-1)
>>> {print(p) for p in r2lobby.PORTALS}
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 0, cell: 2, plane: 0)>
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 4, cell: 2, plane: 1)>
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 8, cell: 2, plane: 2)>
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 12, cell: 2, plane: 3)>
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 16, cell: 2, plane: 4)>
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 20, cell: 2, plane: 5)>
{None}
>>> {print(p) for p in r2lobby.PLANES[:6]}
Plane(normal=vec3(0.0, -1.0, 0.0), distance=-3072.0)
Plane(normal=vec3(-1.0, 0.0, 0.0), distance=-3072.0)
Plane(normal=vec3(1.0, 0.0, 0.0), distance=-3072.0)
Plane(normal=vec3(0.0, 0.0, -1.0), distance=-3072.0)
Plane(normal=vec3(0.0, 0.0, 1.0), distance=-3072.0)
Plane(normal=vec3(0.0, 1.0, 0.0), distance=-3072.0)
{None}
all |
Beta Was this translation helpful? Give feedback.
-
>>> list(f(r2lobby, 0))
[<Vertex (x: 3072.0, y: -3072.0, z: 3072.0)>, <Vertex (x: 3072.0, y: 3072.0, z: 3072.0)>,
<Vertex (x: -3072.0, y: 3072.0, z: 3072.0)>, <Vertex (x: -3072.0, y: 3072.0, z: -3072.0)>,
<Vertex (x: 3072.0, y: 3072.0, z: -3072.0)>, <Vertex (x: 3072.0, y: 3072.0, z: 3072.0)>,
<Vertex (x: 3072.0, y: 3072.0, z: 3072.0)>, <Vertex (x: 3072.0, y: 3072.0, z: -3072.0)>] This should be a Y plane (y=3072), but the first vertex is on the opposite side of the map >>> list(g(r2lobby, 0))
[4, 1, 2, 3, 6, 5, 1, 6] wierd pattern |
Beta Was this translation helpful? Give feedback.
-
This ends the discord dump. |
Beta Was this translation helpful? Give feedback.
-
Digging through Quake & Source vis implementations rn. But Quake doesn't have a Source does have a Going to have to dig deeper into the compiler sources to better understand what gets written to the BSP (might just be the vis-table) |
Beta Was this translation helpful? Give feedback.
-
I might've been overthinking things
>>> import bsp_tool
>>> bsp = bsp_tool.load_bsp("E:/Mod/Titanfall2/maps/mp_lobby.bsp")
>>> bsp.PORTALS[0]
<Portal (is_reversed: 0, type: 1, num_edges: 4, padding: 0, first_reference: 0, cell: 2, plane: 0)>
>>> bsp.PORTAL_EDGES[:4]
[[4, 1], [1, 2], [2, 3], [3, 4]]
>>> bsp.PLANES[0]
Plane(normal=vec3(0.0, -1.0, 0.0), distance=-3072.0) # y = 3072
>>> bsp.PORTAL_VERTICES[1:5] # (1, 2, 3, 4) or [e[1] for e in PORTAL_EDGES[:4]]
[<Vertex (x: 3072.0, y: 3072.0, z: -3072.0)>,
<Vertex (x: 3072.0, y: 3072.0, z: 3072.0)>,
<Vertex (x: -3072.0, y: 3072.0, z: 3072.0)>,
<Vertex (x: -3072.0, y: 3072.0, z: -3072.0)>] Also, >>> {print(i, x) for i, x in enumerate(bsp.PORTAL_VERTEX_EDGES)}
0 PortalIndexSet(index=(-1, -1, -1, -1, -1, -1, -1, -1))
1 PortalIndexSet(index=(0, 1, 6, -1, -1, -1, -1, -1))
2 PortalIndexSet(index=(1, 2, 5, -1, -1, -1, -1, -1))
3 PortalIndexSet(index=(2, 3, 9, -1, -1, -1, -1, -1))
4 PortalIndexSet(index=(0, 3, 8, -1, -1, -1, -1, -1))
5 PortalIndexSet(index=(4, 5, 10, -1, -1, -1, -1, -1))
6 PortalIndexSet(index=(4, 6, 11, -1, -1, -1, -1, -1))
7 PortalIndexSet(index=(7, 8, 11, -1, -1, -1, -1, -1))
8 PortalIndexSet(index=(7, 9, 10, -1, -1, -1, -1, -1))
{None}
>>> bsp.PORTAL_EDGES[0]
[4, 1] # yep
>>> bsp.PORTAL_EDGES[1]
[1, 2] # yep
>>> bsp.PORTAL_EDGES[6]
[1, 6] # yep
...
>>> bsp.PORTAL_EDGES[10]
[5, 8] # yep
>>> bsp.PORTAL_EDGES[11]
[7, 6] # yep
|
Beta Was this translation helpful? Give feedback.
-
Rethinking the lump connections: >>> import bsp_tool
>>> bsp = bsp_tool.load_bsp("E:/Mod/TitanfallOnline/maps/mp_box.bsp")
>>> {print(f"{L:<32} {getattr(bsp, L)!r}") for L, h in bsp.headers.items()
if h.length != 0 and (L.startswith("CELL") or L.startswith("PORTAL"))}
CELL_BSP_NODES <BspLump(51 CellBSPNode) at 0x000001CC53A765E0>
CELLS <BspLump(26 Cell) at 0x000001CC53A76670>
PORTALS <BspLump(225 Portal) at 0x000001CC53A76700>
PORTAL_VERTICES <BspLump(184 Vertex) at 0x000001CC53A76790>
PORTAL_EDGES <BspLump(156 Edge) at 0x000001CC53A767F0>
PORTAL_VERTEX_EDGES <BspLump(184 PortalIndexSet) at 0x000001CC53A76850>
PORTAL_VERTEX_REFERENCES <BasicBspLump(616 UnsignedShorts) at 0x000001CC53A768B0>
PORTAL_EDGE_REFERENCES <BasicBspLump(616 UnsignedShorts) at 0x000001CC53A76910>
PORTAL_EDGE_INTERSECT_AT_EDGE <BspLump(172 PortalIndexSet) at 0x000001CC53A76970>
PORTAL_EDGE_INTERSECT_AT_VERTEX <BspLump(172 PortalIndexSet) at 0x000001CC53A769D0>
PORTAL_EDGE_INTERSECT_HEADER <BspLump(156 PortalEdgeIntersectHeader) at 0x000001CC53A76A30>
CELL_AABB_NODES <BspLump(206 CellAABBNode) at 0x000001CC53A76B80>
{None} do >>> max(bsp.PORTAL_EDGE_REFERENCES)
311 # > len(bsp.PORTAL_EDGES)
>>> (311 // 2), (311 & 1) # indexing edges as a flat array
(155, 1) # last index of bsp.PORTAL_EDGES[155][1] ok, what do >>> import itertools
>>> max(itertools.chain(*bsp.PORTAL_EDGES))
183 # len(PortalVertices) - 1 I guess What about >>> max(bsp.PORTAL_VERTEX_REFERENCES)
183 # len(PortalVertices) - 1
|
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
IW Engine might have a similar system >>> all_branches = {*bsp_tool.branches.quake_based, *bsp_tool.branches.source_based}
>>> {b.__name__ for b in all_branches if any("CELL" in L.name for L in b.LUMP)}
{'bsp_tool.branches.infinity_ward.call_of_duty1',
'bsp_tool.branches.infinity_ward.call_of_duty1_demo',
'bsp_tool.branches.infinity_ward.call_of_duty2',
'bsp_tool.branches.infinity_ward.modern_warfare',
'bsp_tool.branches.respawn.apex_legends',
'bsp_tool.branches.respawn.apex_legends50',
'bsp_tool.branches.respawn.apex_legends51',
'bsp_tool.branches.respawn.titanfall',
'bsp_tool.branches.respawn.titanfall2',
'bsp_tool.branches.respawn.titanfall_x360'} |
Beta Was this translation helpful? Give feedback.
-
If >>> import bsp_tool, os, fnmatch
>>> md = "E:/Mod/Titanfall2/maps"
>>> maps = {m[:-4]: bsp_tool.load_bsp(os.path.join(md, m)) for m in fnmatch.filter(os.listdir(md), "*.bsp")}
>>> {p.cell >= len(b.CELLS) for m, b in maps.items() for p in b.PORTALS if p.type.value == 1}
{True} |
Beta Was this translation helpful? Give feedback.
-
>>> import bsp_tool
>>> bsp = bsp_tool.load_bsp("E:/Mod/Titanfall/maps/mp_runoff.bsp")
>>> bsp.LEAF_WATER_DATA[0]
<LeafWaterData (surface_z: 18.0, min_z: -64.0, texture_info: 80)>
>>> from bsp_tool.utils.vector import vec3
>>> Plane = bsp.PLANES.LumpClass
>>> p = Plane(vec3(z=1), 18) # water surface
>>> bsp.PLANES[::].index(p)
12
>>> {i for i, p in enumerate(bsp.PORTALS) if p.plane == 12} Not every It might be easier to reverse the windings (polygons) of |
Beta Was this translation helpful? Give feedback.
-
Original Header
Dumping research notes on
respawn.titanfall
Cells & Portals fromNorthstar Discord > #research channel > #maps thread
Update
Including
CellAABBNodes
&ObjReferences
findsBeta Was this translation helpful? Give feedback.
All reactions