From 6d76dd18065a8965370d833f9df47b3b621bae5d Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:57:45 +0100 Subject: [PATCH 1/2] Add extraction of spawn points. --- awpy/__init__.py | 4 +- awpy/cli.py | 19 ++++- awpy/nav.py | 119 ++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- scripts/generate-map-spawns.ps1 | 49 +++++++++++++ scripts/generate-nav.ps1 | 2 +- 6 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 scripts/generate-map-spawns.ps1 diff --git a/awpy/__init__.py b/awpy/__init__.py index 4692ddfb..53ef2311 100644 --- a/awpy/__init__.py +++ b/awpy/__init__.py @@ -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"] diff --git a/awpy/cli.py b/awpy/cli.py index 8a78571a..9c73eb23 100644 --- a/awpy/cli.py +++ b/awpy/cli.py @@ -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 @@ -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.") @@ -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}") diff --git a/awpy/nav.py b/awpy/nav.py index bc4f477b..3eb755b8 100644 --- a/awpy/nav.py +++ b/awpy/nav.py @@ -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 @@ -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: + """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) diff --git a/pyproject.toml b/pyproject.toml index 7180d8f8..d1c89d53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/scripts/generate-map-spawns.ps1 b/scripts/generate-map-spawns.ps1 new file mode 100644 index 00000000..895242c6 --- /dev/null +++ b/scripts/generate-map-spawns.ps1 @@ -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 +} diff --git a/scripts/generate-nav.ps1 b/scripts/generate-nav.ps1 index 760bb077..7e386f52 100644 --- a/scripts/generate-nav.ps1 +++ b/scripts/generate-nav.ps1 @@ -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 { From c6c660ba217791309e215ad3a488304aeb133244 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:44:10 +0100 Subject: [PATCH 2/2] Remove Position and use Vector3 --- awpy/nav.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/awpy/nav.py b/awpy/nav.py index 3eb755b8..c114f8b3 100644 --- a/awpy/nav.py +++ b/awpy/nav.py @@ -437,21 +437,13 @@ def to_json(self, path: str | Path) -> None: VentsValue = str | int | float | bool | tuple[float, ...] -@dataclass -class Position: - """Simple class.""" - - x: float - y: float - z: float - @dataclass class Spawns: """Spawns of a map.""" - CT: list[Position] - T: list[Position] + CT: list[Vector3] + T: list[Vector3] def to_dict(self) -> dict[str, list[dict[str, float]]]: """Converts the spawns to a dictionary.""" @@ -525,8 +517,8 @@ def parse_file_to_dict(file_content: str) -> dict[int, dict[str, VentsValue]]: 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] = [] + ct_spawns: list[Vector3] = [] + t_spawns: list[Vector3] = [] for properties in data.values(): if ( @@ -536,7 +528,7 @@ def filter_data(data: dict[int, dict[str, VentsValue]]) -> Spawns: ): x, y, z = properties["origin"] # pyright: ignore[reportGeneralTypeIssues] t_spawns.append( - Position(x=x, y=y, z=z) # pyright: ignore[reportArgumentType] + Vector3(x=x, y=y, z=z) # pyright: ignore[reportArgumentType] ) elif ( properties.get("classname") == "info_player_counterterrorist" @@ -545,7 +537,7 @@ def filter_data(data: dict[int, dict[str, VentsValue]]) -> Spawns: ): x, y, z = properties["origin"] # pyright: ignore[reportGeneralTypeIssues] ct_spawns.append( - Position(x=x, y=y, z=z) # pyright: ignore[reportArgumentType] + Vector3(x=x, y=y, z=z) # pyright: ignore[reportArgumentType] ) return Spawns(CT=ct_spawns, T=t_spawns)