Skip to content

Commit dcc7e50

Browse files
committed
Real Initial Commit
1 parent 3a31d0c commit dcc7e50

11 files changed

Lines changed: 10800 additions & 1 deletion

File tree

.github/workflows/tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Code Tests
2+
on: [push, pull_request]
3+
jobs:
4+
Tests:
5+
runs-on: ubuntu-latest
6+
container:
7+
image: ubuntu:24.04
8+
steps:
9+
- name: Update System Packages
10+
run: |
11+
apt update && apt install git python3 python3-pip -y
12+
shell: bash
13+
- name: Checkout Repository
14+
uses: actions/checkout@v4
15+
with:
16+
submodules: recursive
17+
- name: Install pip Packages
18+
working-directory: ./
19+
run: |
20+
python3 -m pip install -r requirements.txt --break-system-packages
21+
- name: Output Matching Test
22+
working-directory: ./
23+
run: |
24+
bash -ex tests/output_matching.sh

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "p3d_libmap"]
2+
path = p3d_libmap
3+
url = https://github.com/csevier/p3d_libmap

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
# spawn-zone-tool
2-
Python Tool for Creating Nazi Zombies: Portable Spawn Zone files
2+
3+
## About
4+
5+
Python Tool for Creating Nazi Zombies: Portable Spawn Zone files, which define segmented areas for larger levels to control the flow of AI spawning as well as minor cosmetic changes.
6+
7+
## Usage
8+
9+
Provide your virtual environment is set up with `requirements.txt` installed:
10+
```bash
11+
python3 spawn_zone_tool.py path/to/your/map --output output/path/for/nsz
12+
```

p3d_libmap

Submodule p3d_libmap added at 965a257

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Panda3D==1.10.15

spawn_zone_tool.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
from __future__ import annotations
2+
import argparse
3+
from dataclasses import dataclass, field
4+
from pathlib import Path
5+
from typing import Dict, List, Tuple, Optional
6+
7+
import sys
8+
9+
SCRIPT_DIR = Path(__file__).resolve().parent
10+
sys.path.insert(0, str(SCRIPT_DIR))
11+
12+
from p3d_libmap.map_parser import MapParser
13+
14+
15+
# ---------------------------------------------------------------------------
16+
# Data Structures
17+
# ---------------------------------------------------------------------------
18+
19+
@dataclass
20+
class ZoneBrush:
21+
mins: Tuple[float, float, float]
22+
maxs: Tuple[float, float, float]
23+
24+
25+
@dataclass
26+
class Zone:
27+
name: str
28+
zone_id: int
29+
target: str
30+
fog: str = ""
31+
adjacent_zones: List[int] = field(default_factory=list)
32+
brushes: List[ZoneBrush] = field(default_factory=list)
33+
34+
35+
# ---------------------------------------------------------------------------
36+
# Map utilities
37+
# ---------------------------------------------------------------------------
38+
39+
def load_map_without_geo(path: Path):
40+
"""Load a .map file but do not parse brush geometry."""
41+
parser = MapParser()
42+
parser.parser_load(str(path))
43+
return parser.map_data
44+
45+
46+
def get_bounds_for_brush(brush, origin=(0.0, 0.0, 0.0)):
47+
"""Compute min/max bounds for a brush."""
48+
ox, oy, oz = origin
49+
50+
xs, ys, zs = [], [], []
51+
52+
for face in brush.faces:
53+
pts = (face.plane_points.v0, face.plane_points.v1, face.plane_points.v2)
54+
for p in pts:
55+
xs.append(p.x + ox)
56+
ys.append(p.y + oy)
57+
zs.append(p.z + oz)
58+
59+
if not xs:
60+
return None, None
61+
62+
return (min(xs), min(ys), min(zs)), (max(xs), max(ys), max(zs))
63+
64+
65+
# ---------------------------------------------------------------------------
66+
# Zone file output
67+
# ---------------------------------------------------------------------------
68+
69+
def write_zones_to_file(zones: List[Zone], output_path: Path):
70+
"""Write all zones to an .nsz file."""
71+
with output_path.open("w") as f:
72+
f.write("zone_file_version: 1.0.0\n")
73+
f.write(f"number_of_zones: {len(zones)}\n")
74+
75+
for zone in zones:
76+
f.write(f"{zone.name}\n")
77+
f.write(f"{zone.zone_id}\n")
78+
f.write(f"{zone.target}\n")
79+
f.write(f"{zone.fog}\n")
80+
81+
f.write(f"{len(zone.adjacent_zones)}\n")
82+
for adj in zone.adjacent_zones:
83+
f.write(f"{adj}\n")
84+
85+
f.write(f"{len(zone.brushes)}\n")
86+
for b in zone.brushes:
87+
f.write(f"{b.mins[0]} {b.mins[1]} {b.mins[2]}\n")
88+
f.write(f"{b.maxs[0]} {b.maxs[1]} {b.maxs[2]}\n")
89+
90+
91+
# ---------------------------------------------------------------------------
92+
# Main Processing Logic
93+
# ---------------------------------------------------------------------------
94+
95+
def process_map(map_data):
96+
zone_name_to_id: Dict[str, int] = {}
97+
next_zone_id = 1
98+
zones: List[Zone] = []
99+
100+
def get_id_for_zone(name: str) -> int:
101+
nonlocal next_zone_id
102+
if name not in zone_name_to_id:
103+
zone_name_to_id[name] = next_zone_id
104+
next_zone_id += 1
105+
return zone_name_to_id[name]
106+
107+
print(f"Total entities: {len(map_data.entities)}\n")
108+
109+
for ent in map_data.entities:
110+
if ent.properties.get("classname") != "spawn_zone":
111+
continue
112+
113+
zone_name = ent.properties.get("zone_name")
114+
zone_target = ent.properties.get("zone_target")
115+
zone_fog = ent.properties.get("zone_fog", "")
116+
117+
print(f"+ Found a spawn_zone entity:")
118+
print(f" - Name: {zone_name}")
119+
print(f" - Target: {zone_target}")
120+
print(f" - Fog: {zone_fog}")
121+
122+
# Resolve adjacent zones
123+
adjacents = ent.properties.get("adjacent_zones", "")
124+
adjacent_zones = [
125+
get_id_for_zone(z.strip())
126+
for z in adjacents.split(",")
127+
if z.strip()
128+
]
129+
130+
print(f" - Adjacent: {adjacent_zones}")
131+
132+
# Zone brushes
133+
brushes = []
134+
for i, brush in enumerate(ent.brushes):
135+
mins, maxs = get_bounds_for_brush(brush)
136+
brushes.append(ZoneBrush(mins=mins, maxs=maxs))
137+
print(f" * Brush {i}: mins={mins}, maxs={maxs}")
138+
139+
zone_id = get_id_for_zone(zone_name)
140+
141+
zones.append(
142+
Zone(
143+
name=zone_name,
144+
zone_id=zone_id,
145+
target=zone_target,
146+
fog=zone_fog,
147+
adjacent_zones=adjacent_zones,
148+
brushes=brushes,
149+
)
150+
)
151+
152+
print()
153+
154+
return zones
155+
156+
157+
# ---------------------------------------------------------------------------
158+
# Entry Point
159+
# ---------------------------------------------------------------------------
160+
161+
def main():
162+
parser = argparse.ArgumentParser(description="Creates NSZ (NZ:P Spawn Zones) file for use in maps.")
163+
parser.add_argument(
164+
"map_file",
165+
type=Path,
166+
nargs="?",
167+
help="Path to the .map file to create NSZ from",
168+
)
169+
parser.add_argument(
170+
"-o", "--output",
171+
type=Path,
172+
help="Output path for NSZ"
173+
)
174+
175+
args = parser.parse_args()
176+
177+
print(f"Loading map: {args.map_file}")
178+
map_data = load_map_without_geo(args.map_file)
179+
180+
zones = process_map(map_data)
181+
182+
print(f"Writing {len(zones)} zones to: {args.output}")
183+
write_zones_to_file(zones, args.output)
184+
185+
186+
if __name__ == "__main__":
187+
main()

0 commit comments

Comments
 (0)