Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extraction of spawn points. #359

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions awpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Provides data parsing, analytics and visualization capabilities for CSGO data."""

from awpy.demo import Demo
from awpy.nav import Nav
from awpy.nav import Nav, Spawns

__version__ = "2.0.0b5"
__all__ = ["Demo", "Nav"]
__all__ = ["Demo", "Nav", "Spawns"]
19 changes: 17 additions & 2 deletions awpy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from loguru import logger
from tqdm import tqdm

from awpy import Demo, Nav
from awpy import Demo, Nav, Spawns
from awpy.data import AWPY_DATA_DIR, TRI_URL
from awpy.vis import VphysParser

Expand Down Expand Up @@ -105,6 +105,21 @@ def parse_demo(
demo.compress(outpath=outpath)


@awpy.command(help="Parse spawns from a Counter-Strike 2 vent file.")
@click.argument("vent_file", type=click.Path(exists=True))
@click.option("--outpath", type=click.Path(), help="Path to save the compressed demo.")
def parse_spawns(vent_file: Path, *, outpath: Optional[Path] = None) -> None:
"""Parse a nav file given its path."""
vent_file = Path(vent_file)
if not outpath:
output_path = vent_file.with_suffix(".json")
spawns_data = Spawns.from_vents_file(vent_file)
spawns_data.to_json(path=output_path)
logger.success(
f"Spawns file saved to {vent_file.with_suffix('.json')}, {spawns_data}"
)


@awpy.command(help="Parse a Counter-Strike 2 nav file.")
@click.argument("nav_file", type=click.Path(exists=True))
@click.option("--outpath", type=click.Path(), help="Path to save the compressed demo.")
Expand All @@ -113,7 +128,7 @@ def parse_nav(nav_file: Path, *, outpath: Optional[Path] = None) -> None:
nav_file = Path(nav_file)
nav_mesh = Nav(path=nav_file)
if not outpath:
output_path = Path(nav_file.stem + ".json")
output_path = nav_file.with_suffix(".json")
nav_mesh.to_json(path=output_path)
logger.success(f"Nav mesh saved to {nav_file.with_suffix('.json')}, {nav_mesh}")

Expand Down
119 changes: 119 additions & 0 deletions awpy/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import json
import math
import re
import struct
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, BinaryIO, Literal, Optional
Expand Down Expand Up @@ -430,3 +432,120 @@ def to_json(self, path: str | Path) -> None:
nav_dict = self.to_dict()
with open(path, "w", encoding="utf-8") as json_file:
json.dump(nav_dict, json_file)


VentsValue = str | int | float | bool | tuple[float, ...]


@dataclass
class Position:
JanEricNitschke marked this conversation as resolved.
Show resolved Hide resolved
"""Simple class."""

x: float
y: float
z: float


@dataclass
class Spawns:
"""Spawns of a map."""

CT: list[Position]
T: list[Position]

def to_dict(self) -> dict[str, list[dict[str, float]]]:
"""Converts the spawns to a dictionary."""
return {
"CT": [{"x": ct.x, "y": ct.y, "z": ct.z} for ct in self.CT],
"T": [{"x": t.x, "y": t.y, "z": t.z} for t in self.T],
}

def to_json(self, path: str | Path) -> None:
"""Writes the spawns data to a JSON file.

Args:
path: Path to the JSON file to write.
"""
spawns_dict = self.to_dict()
with open(path, "w", encoding="utf-8") as json_file:
json.dump(spawns_dict, json_file)

@staticmethod
def from_vents_content(vents_content: str) -> "Spawns":
"""Parse the content of a vents file into Spawns information."""
parsed_data = parse_file_to_dict(vents_content)

return filter_data(parsed_data)

@staticmethod
def from_vents_file(vents_file: str | Path) -> "Spawns":
"""Parse the content of a vents file into Spawns information."""
with open(vents_file) as f:
return Spawns.from_vents_content(f.read())


def parse_file_to_dict(file_content: str) -> dict[int, dict[str, VentsValue]]:
"""Parse the file content."""
# Dictionary to hold parsed data
parsed_data: dict[int, dict[str, VentsValue]] = {}
block_id = 0
block_content: dict[str, VentsValue] = {}

for line in file_content.splitlines():
if match := re.match(r"^====(\d+)====$", line):
block_id = int(match.group(1))
block_content = {}
continue

if not line.strip():
continue
try:
key, value = line.split(maxsplit=1)
except Exception: # noqa: S112
continue
key = key.strip()
value = value.strip()

# Attempt to parse the value
if value in ("True", "False"):
value = value == "True" # Convert to boolean
elif re.match(r"^-?\d+$", value):
value = int(value) # Convert to integer
elif re.match(r"^-?\d*\.\d+$", value):
value = float(value) # Convert to float
elif re.match(r"^-?\d*\.\d+(?:\s-?\d*\.\d+)+$", value):
value = tuple(map(float, value.split())) # Convert to tuple of floats

block_content[key] = value

parsed_data[block_id] = block_content

return parsed_data


def filter_data(data: dict[int, dict[str, VentsValue]]) -> Spawns:
"""Filter the data to get the positions."""
ct_spawns: list[Position] = []
t_spawns: list[Position] = []

for properties in data.values():
if (
properties.get("classname") == "info_player_terrorist"
and properties.get("enabled")
and properties.get("priority") == 0
):
x, y, z = properties["origin"] # pyright: ignore[reportGeneralTypeIssues]
t_spawns.append(
Position(x=x, y=y, z=z) # pyright: ignore[reportArgumentType]
)
elif (
properties.get("classname") == "info_player_counterterrorist"
and properties.get("enabled")
and properties.get("priority") == 0
):
x, y, z = properties["origin"] # pyright: ignore[reportGeneralTypeIssues]
ct_spawns.append(
Position(x=x, y=y, z=z) # pyright: ignore[reportArgumentType]
)

return Spawns(CT=ct_spawns, T=t_spawns)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ select = [
"RUF",
"EM"
]
ignore = ["D208", "T20", "PTH", "TRY003", "BLE001", "PLR2004", "UP007", "ISC001"]
ignore = ["D208", "T20", "PTH", "TRY003", "BLE001", "PLR2004", "UP007", "ISC001", "ANN101"]
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.pydocstyle]
Expand Down
49 changes: 49 additions & 0 deletions scripts/generate-map-spawns.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This script generates .json files containing CS2 .nav information.

# Define the directory containing .vpk files
$sourcePath = "C:\Program Files (x86)\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\maps"

# Get the current directory where the script is run
$outputDirectory = (Get-Location).Path

# Ensure the path exists
if (Test-Path $sourcePath) {
# Get all .vpk files in the directory
Get-ChildItem -Path $sourcePath -Filter "*.vpk" | ForEach-Object {
# Get full path and base name of the file
$filePath = $_.FullName
$fileNameWithoutExtension = $_.BaseName

# Temporary output directory for the tool
$tempOutputDir = Join-Path -Path $outputDirectory -ChildPath $fileNameWithoutExtension

# Create a temporary directory for the output
if (-Not (Test-Path $tempOutputDir)) {
New-Item -ItemType Directory -Path $tempOutputDir | Out-Null
}

# Construct and run the Source2Viewer-CLI command
Write-Host "Processing file: $filePath" -ForegroundColor Green
.\Source2Viewer-CLI.exe -i $filePath -e "vents_c" -o $tempOutputDir -d

# Move the output file to the current directory and rename it
$generatedFile = Join-Path -Path $tempOutputDir -ChildPath "maps\$fileNameWithoutExtension\entities\default_ents.vents"
$newFileName = Join-Path -Path $outputDirectory -ChildPath "$fileNameWithoutExtension.vents"

if (Test-Path $generatedFile) {
Move-Item -Path $generatedFile -Destination $newFileName -Force
Write-Host "Output saved as: $newFileName" -ForegroundColor Cyan

# Run the awpy parse-spawns command
Write-Host "Running awpy parse-spawns on: $newFileName" -ForegroundColor Yellow
awpy parse-spawns $newFileName
} else {
Write-Host "Error: Expected output file not found for $fileNameWithoutExtension" -ForegroundColor Red
}

# Clean up the temporary directory
Remove-Item -Path $tempOutputDir -Recurse -Force
}
} else {
Write-Host "The specified directory does not exist: $sourcePath" -ForegroundColor Red
}
2 changes: 1 addition & 1 deletion scripts/generate-nav.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if (Test-Path $sourcePath) {
Move-Item -Path $generatedFile -Destination $newFileName -Force
Write-Host "Output saved as: $newFileName" -ForegroundColor Cyan

# Run the awpy generate-tri command
# Run the awpy parse-nav command
Write-Host "Running awpy parse-nav on: $newFileName" -ForegroundColor Yellow
awpy parse-nav $newFileName
} else {
Expand Down