From 156bc3fe3c8423b1533ebe8250a6f6e968848178 Mon Sep 17 00:00:00 2001 From: EonZeNx Date: Wed, 11 Dec 2024 16:35:10 +1300 Subject: [PATCH] feat: init --- .gitignore | 10 ++ blender_main.py | 17 +++ main.py | 30 ++++++ py_atl/__init__.py | 47 ++++++++ py_atl/database/__init__.py | 38 +++++++ py_atl/database/query.py | 17 +++ py_atl/development.py | 6 ++ py_atl/dll/__init__.py | 41 +++++++ py_atl/dll/cs_to.py | 21 ++++ py_atl/rtpc_v01/__init__.py | 19 ++++ py_atl/rtpc_v01/containers.py | 55 ++++++++++ py_atl/utils.py | 17 +++ pysharp.py | 194 ++++++++++++++++++++++++++++++++++ 13 files changed, 512 insertions(+) create mode 100644 .gitignore create mode 100644 blender_main.py create mode 100644 main.py create mode 100644 py_atl/__init__.py create mode 100644 py_atl/database/__init__.py create mode 100644 py_atl/database/query.py create mode 100644 py_atl/development.py create mode 100644 py_atl/dll/__init__.py create mode 100644 py_atl/dll/cs_to.py create mode 100644 py_atl/rtpc_v01/__init__.py create mode 100644 py_atl/rtpc_v01/containers.py create mode 100644 py_atl/utils.py create mode 100644 pysharp.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd45c07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +.venv +__pycache__ + +test + +py_atl/dll/**.dll +py_atl/dll/**.json + +**.db \ No newline at end of file diff --git a/blender_main.py b/blender_main.py new file mode 100644 index 0000000..f0ffced --- /dev/null +++ b/blender_main.py @@ -0,0 +1,17 @@ +if __name__ == '__main__': + try: + import clr_loader + except ImportError: + import pip + pip.main(['install', 'clr-loader']) + import clr_loader + + try: + import pythonnet + except ImportError: + import pip + pip.main(["install", "pythonnet"]) + import pythonnet + + from main import setup_dll + setup_dll() diff --git a/main.py b/main.py new file mode 100644 index 0000000..056a128 --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +import os +import pythonnet +from py_atl import database + +PROJECT_DIRECTORY: str = r"D:\Projects\Various\pysharp" +DLL_DIRECTORY: str = os.path.join(PROJECT_DIRECTORY, "dll") +DB_PATH: str = os.path.join(PROJECT_DIRECTORY, "database", "atl.jc3.db") + + +def setup_dll(): + global DLL_DIRECTORY + + from os import listdir + from os.path import isfile, join + + pythonnet.load("coreclr") + + import clr + + dll_files: list[str] = [f.removesuffix(".dll") for f in listdir(DLL_DIRECTORY) if isfile(join(DLL_DIRECTORY, f)) and f.endswith(".dll")] + for dll_file in dll_files: + clr.AddReference(dll_file) + + import pysharp + pysharp.main() + + +if __name__ == '__main__': + setup_dll() + database.setup(DB_PATH) diff --git a/py_atl/__init__.py b/py_atl/__init__.py new file mode 100644 index 0000000..923acd0 --- /dev/null +++ b/py_atl/__init__.py @@ -0,0 +1,47 @@ +bl_info = { + "name": "PyAtl", + "blender": (4, 3, 0), + "category": "Import-Export", + "description": "Interfaces with ATL", + "author": "EonZeNx", + "version": (0, 1, 0), +} + +from py_atl import utils, dll +import bpy +from bpy.props import StringProperty +from bpy.types import AddonPreferences + + +class PyAtlPreferences(AddonPreferences): + bl_idname = __name__ + + project_path: StringProperty( + name="Project Base Path", + description="Base path for dll files, database files, and more", + default="", + subtype='DIR_PATH', + update=lambda self, context: self.on_preference_update(context) + ) + + def draw(self, context): + layout = self.layout + layout.prop(self, "project_path") + + def on_preference_update(self, context): + dll.setup_dll(self.project_path) + + +def register(): + bpy.utils.register_class(PyAtlPreferences) + + from py_atl import utils, dll + dll.setup_dll(utils.project_path(), True) + + +def unregister(): + bpy.utils.unregister_class(PyAtlPreferences) + + +if __name__ == "__main__": + register() diff --git a/py_atl/database/__init__.py b/py_atl/database/__init__.py new file mode 100644 index 0000000..e482d8e --- /dev/null +++ b/py_atl/database/__init__.py @@ -0,0 +1,38 @@ +from ApexToolsLauncher.Core.Hash import HashDatabase, EHashType + +DB_CONNECTION = None + + +def setup(db_path: str) -> None: + global DB_CONNECTION + + if DB_CONNECTION is not None: + return + + option_database = HashDatabase.Create(db_path) + if option_database.IsNone: + print(f"failed to setup hash database") + return + + DB_CONNECTION = option_database.Unwrap() + DB_CONNECTION.OpenConnection() + + +def lookup(game_hash: int): + global DB_CONNECTION + + if DB_CONNECTION is None: + print(f"lookup_filepath : DB_CONNECTION is None") + return None + + return DB_CONNECTION.Lookup(game_hash) + + +def lookup_filepath(game_hash: int): + global DB_CONNECTION + + if DB_CONNECTION is None: + print(f"lookup_filepath : DB_CONNECTION is None") + return None + + return DB_CONNECTION.Lookup(game_hash, EHashType.FilePath) diff --git a/py_atl/database/query.py b/py_atl/database/query.py new file mode 100644 index 0000000..078a107 --- /dev/null +++ b/py_atl/database/query.py @@ -0,0 +1,17 @@ +from py_atl.database import DB_CONNECTION +from ApexToolsLauncher.Core.Hash import HashDatabase, EHashType + + +def lookup(game_hash: int): + if DB_CONNECTION is None: + return None + + return DB_CONNECTION.Lookup(game_hash) + + +def lookup_filepath(game_hash: int): + if DB_CONNECTION is None: + print(f"lookup_filepath : DB_CONNECTION is None") + return None + + return DB_CONNECTION.Lookup(game_hash, EHashType.FilePath) diff --git a/py_atl/development.py b/py_atl/development.py new file mode 100644 index 0000000..a4216e7 --- /dev/null +++ b/py_atl/development.py @@ -0,0 +1,6 @@ + +DLL_FILES: list[str] = [] + + +def log(msg: str) -> None: + print(f"PyAtl: {msg}") diff --git a/py_atl/dll/__init__.py b/py_atl/dll/__init__.py new file mode 100644 index 0000000..4f0059d --- /dev/null +++ b/py_atl/dll/__init__.py @@ -0,0 +1,41 @@ +import pythonnet +if pythonnet.get_runtime_info() is not None: + pythonnet.unload() + +pythonnet.load("coreclr") +import clr + +from py_atl import development, utils +from os import path, listdir + + +def setup_dll(project_path: str, log_result: bool = False): + if len(project_path) == 0 or not path.exists(project_path): + return + + dll_directory: str = path.join(project_path, "dll") + development.DLL_FILES = [path.join(dll_directory, f).removesuffix(".dll") + for f in listdir(dll_directory) + if path.isfile(path.join(dll_directory, f)) and f.endswith(".dll")] + + loaded_dlls: list[str] = [] + failed_dlls: list[str] = [] + for dll_file in development.DLL_FILES: + try: + clr.AddReference(dll_file) + loaded_dlls.append(dll_file) + except (Exception,) as e: + development.log(e) + failed_dlls.append(dll_file) + + if log_result: + development.log(f"Successfully loaded {len(loaded_dlls)} dll files") + for dll in loaded_dlls: + development.log(f"\t- {dll}") + + development.log(f"Failed to load {len(failed_dlls)}") + for dll in failed_dlls: + development.log(f"\t- {dll}") + + +setup_dll(utils.project_path()) diff --git a/py_atl/dll/cs_to.py b/py_atl/dll/cs_to.py new file mode 100644 index 0000000..9fb2bea --- /dev/null +++ b/py_atl/dll/cs_to.py @@ -0,0 +1,21 @@ +""" +functions to convert objects from c# to python +""" + +from System import Array, Single + +from mathutils import Matrix + + +def bpy_matrix(mat4: Array[Single]) -> Matrix: + if len(mat4) != 16: + raise ValueError(f"Expected 16 elements, got {len(mat4)}") + + matrix: Matrix = Matrix(( + (mat4[0], mat4[1], mat4[2], mat4[3]), + (mat4[4], mat4[5], mat4[6], mat4[7]), + (mat4[8], mat4[9], mat4[10], mat4[11]), + (mat4[12], mat4[13], mat4[14], mat4[15]) + )) + + return matrix diff --git a/py_atl/rtpc_v01/__init__.py b/py_atl/rtpc_v01/__init__.py new file mode 100644 index 0000000..0fb5a8b --- /dev/null +++ b/py_atl/rtpc_v01/__init__.py @@ -0,0 +1,19 @@ +from System.IO import FileStream, FileMode +from System import Array, String +from System.Collections.Generic import Dictionary +from ApexFormat.RTPC.V01.Class import RtpcV01HeaderExtensions, RtpcV01ContainerExtensions, RtpcV01Container, IRtpcV01Filter, RtpcV01Filter, RtpcV01FilterString + + +def filter_by(filters: list[IRtpcV01Filter]): + for each in filters: + print(each) + + +def filter_by_test(): + rigid_object: RtpcV01FilterString = RtpcV01FilterString("_class", "CRigidObject") + rigid_object.SubFilters = [ + RtpcV01Filter("filename"), + RtpcV01Filter("world") + ] + + filter_by([rigid_object]) diff --git a/py_atl/rtpc_v01/containers.py b/py_atl/rtpc_v01/containers.py new file mode 100644 index 0000000..5828a80 --- /dev/null +++ b/py_atl/rtpc_v01/containers.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass, field +from typing import Self + + +@dataclass(slots=True) +class RtpcObject: + init_value: str | int | None = field(init=True, default=None) + name: str | None = field(init=False, default=None) + name_hash: int | None = field(init=False, default=None) + containers: list[Self] = field(init=False, default_factory=list) + + def __post_init__(self): + if type(self.init_value) is str: + self.name = self.init_value + elif type(self.init_value) is int: + self.name_hash = self.init_value + else: + raise ValueError(f"{type(self).__name__} initialised with invalid type, {type(self.init_value)}") + + if self.name is None and self.name_hash is None: + raise ValueError(f"{type(self).__name__} must have name or name_hash") + + def __repr__(self): + name_or_hash: str = self.name if self.name is not None else f"{self.name_hash:08X}" + containers_str: str = f"containers: {self.containers}" if len(self.containers) > 0 else "" + containers_prefix: str = ", " if len(containers_str) > 0 else "" + + return f"{type(self).__name__}({name_or_hash}{containers_prefix}{containers_str})" + + +@dataclass(slots=True) +class RtpcWorldObject(RtpcObject): + world: str = field(init=False, default=None) + + def __repr__(self): + name_or_hash: str = self.name if self.name is not None else f"{self.name_hash:08X}" + containers_str: str = f"containers: {self.containers}" if len(self.containers) > 0 else "" + containers_prefix: str = ", " if len(containers_str) > 0 else "" + + return f"{type(self).__name__}({name_or_hash}, world: {self.world}{containers_prefix}{containers_str})" + + +@dataclass(slots=True) +class RtpcRigidObject(RtpcWorldObject): + filename: str | None = field(init=False, default=None) + filename_hash: int | None = field(init=False, default=None) + + def __repr__(self): + name_or_hash: str = self.name if self.name is not None else f"{self.name_hash:08X}" + containers_str: str = f"containers: {self.containers}" if len(self.containers) > 0 else "" + containers_prefix: str = ", " if len(containers_str) > 0 else "" + filename_or_hash: str = self.filename if self.filename is not None else f"{self.filename_hash:08X}" + + return f"{type(self).__name__}({name_or_hash}, filename: {filename_or_hash}, world: {self.world}{containers_prefix}{containers_str})" + diff --git a/py_atl/utils.py b/py_atl/utils.py new file mode 100644 index 0000000..9227b18 --- /dev/null +++ b/py_atl/utils.py @@ -0,0 +1,17 @@ +import bpy + + +def project_path() -> str: + addon = bpy.context.preferences.addons.get("py_atl", None) + if addon is None: + return "" + + preferences = addon.preferences + if preferences is None: + return "" + + project_path = preferences.project_path + if project_path is None: + return "" + + return project_path diff --git a/pysharp.py b/pysharp.py new file mode 100644 index 0000000..ec34eee --- /dev/null +++ b/pysharp.py @@ -0,0 +1,194 @@ +""" +This file expects pythonnet to already have loaded all relevant dll +""" + +from py_atl import database, development +from py_atl.dll import cs_to +from py_atl.rtpc_v01.containers import RtpcObject, RtpcWorldObject, RtpcRigidObject + +# these may produce expected warnings +from System.IO import FileStream, FileMode +from ApexFormat.RTPC.V01.Class import (RtpcV01HeaderExtensions, RtpcV01ContainerExtensions, RtpcV01Container, + RtpcV01Variant, IRtpcV01Filter, RtpcV01Filter, RtpcV01FilterString, + RtpcV01VariantExtensions, RtpcV01VariantHeaderExtensions) +from ApexToolsLauncher.Core.Hash import JenkinsL3 + + +FILE_PATH: str = r"D:\Games\Avalanche\_Projects\py-atl\test\dlc3_satellite_base_01.blo" +DB_PATH: str = r"D:\Games\Avalanche\_Projects\py-atl\py_atl\database\atl.jc3.db" + + +def get_property(container: RtpcV01Container, property_id: str | int) -> RtpcV01Variant | None: + property_hash: int = 0 + if type(property_id) is int: + property_hash = property_id + elif type(property_id) is str: + property_hash = JenkinsL3.Jenkins(property_id) + else: + raise ValueError(f"RtpcObject initialised with invalid type, {type(property_id)}") + + for container_property in container.Properties: + if container_property.NameHash == property_hash: + return container_property + + return None + + +def create_rtpc_rigid_object(container: RtpcV01Container) -> RtpcRigidObject | None: + name_property = get_property(container, "name") + if name_property is None: + development.log(f"container did not have name property") + return None + + world_property = get_property(container, "world") + if world_property is None: + development.log(f"container did not have world property") + return None + + filename_hash_property = get_property(container, "filename") + if filename_hash_property is None: + development.log(f"container did not have filename property") + return None + + filename_hash_value: int = RtpcV01VariantHeaderExtensions.AsInt(filename_hash_property) + + option_filename_result = database.lookup_filepath(filename_hash_value) + if option_filename_result.IsNone: + development.log(f"failed to find option filename") + return None + + name_value: str = RtpcV01VariantExtensions.AsString(name_property) + world_value = cs_to.bpy_matrix(RtpcV01VariantExtensions.AsMat4X4(world_property)) + filename: str = option_filename_result.Unwrap().Value + + rigid_object: RtpcRigidObject = RtpcRigidObject(name_value) + rigid_object.world = world_value + rigid_object.filename = filename + + return rigid_object + + +def create_rtpc_object(container: RtpcV01Container, recurse: bool = True) -> RtpcWorldObject | None: + class_property = get_property(container, "_class") + if class_property is None: + return None + + class_value: str = RtpcV01VariantExtensions.AsString(class_property) + + rtpc_object: RtpcWorldObject | None = None + if class_value == "CRigidObject": + rtpc_object = create_rtpc_rigid_object(container) + + if recurse: + for child_container in container.Containers: + child_object = create_rtpc_object(child_container, recurse) + if child_object is not None: + rtpc_object.containers.append(child_object) + + return rtpc_object + + +def main() -> None: + global FILE_PATH, DB_PATH + + in_buffer = FileStream(FILE_PATH, FileMode.Open) + + option_header = RtpcV01HeaderExtensions.ReadRtpcV01Header(in_buffer) + if option_header.IsNone: + return + + option_container = RtpcV01ContainerExtensions.ReadRtpcV01Container(in_buffer) + if option_container.IsNone: + return + + container: RtpcV01Container = option_container.Unwrap() + + rigid_object: RtpcV01FilterString = RtpcV01FilterString("_class", "CRigidObject") + rigid_object.SubFilters = [ + RtpcV01Filter("name"), + RtpcV01Filter("filename"), + RtpcV01Filter("world") + ] + + effect_point_emitter: RtpcV01FilterString = RtpcV01FilterString("_class", "CEffectPointEmitter") + effect_point_emitter.SubFilters = [ + RtpcV01Filter("name"), + RtpcV01Filter("effect"), + RtpcV01Filter("world") + ] + + dynamic_light_object: RtpcV01FilterString = RtpcV01FilterString("_class", "CDynamicLightObject") + dynamic_light_object.SubFilters = [ + RtpcV01Filter("name"), + RtpcV01Filter("diffuse"), + RtpcV01Filter("falloff_start"), + RtpcV01Filter("is_spot_light"), + RtpcV01Filter("multiplier"), + RtpcV01Filter("on_during_daytime"), + RtpcV01Filter("projected_texture"), + RtpcV01Filter("projected_texture_enabled"), + RtpcV01Filter("projected_texture_u_scale"), + RtpcV01Filter("projected_texture_v_scale"), + RtpcV01Filter("radius"), + RtpcV01Filter("spot_angle"), + RtpcV01Filter("spot_inner_angle"), + RtpcV01Filter("volume_intensity"), + RtpcV01Filter("volumetric_mode"), + RtpcV01Filter("world"), + ] + + static_decal_object: RtpcV01FilterString = RtpcV01FilterString("_class", "CStaticDecalObject") + static_decal_object.SubFilters = [ + RtpcV01Filter("name"), + RtpcV01Filter("Emissive"), + RtpcV01Filter("alpha_max"), + RtpcV01Filter("alpha_min"), + RtpcV01Filter("alphamask_offset_u"), + RtpcV01Filter("alphamask_offset_v"), + RtpcV01Filter("alphamask_source_channel"), + RtpcV01Filter("alphamask_strength"), + RtpcV01Filter("alphamask_texture"), + RtpcV01Filter("alphamask_tile_u"), + RtpcV01Filter("alphamask_tile_v"), + RtpcV01Filter("color"), + RtpcV01Filter("diffuse_texture"), + RtpcV01Filter("distance_field_decal_mpm"), + RtpcV01Filter("is_distance_field_stencil"), + RtpcV01Filter("offset_u"), + RtpcV01Filter("offset_v"), + RtpcV01Filter("tile_u"), + RtpcV01Filter("tile_v"), + RtpcV01Filter("world"), + ] + + filters: list[IRtpcV01Filter] = [ + rigid_object, + ] + + total_before: int = RtpcV01ContainerExtensions.CountContainers(container) + RtpcV01ContainerExtensions.FilterBy(container, filters) + total_after: int = RtpcV01ContainerExtensions.CountContainers(container) + + development.log(f"total pre-filter: {total_before}, total post-filter: {total_after}") + + database.setup(DB_PATH) + py_container = RtpcObject("root") + + for i in range(len(container.Containers)): + child_container = container.Containers[i] + + world_object: RtpcWorldObject = create_rtpc_object(child_container) + if world_object is not None: + py_container.containers.append(world_object) + + if i >= 10: + break + + development.log(f"{py_container}") + + +if __name__ == '__main__': + import pythonnet + pythonnet.load("coreclr") + + main()