Source code for pyunity.files

"""
Module to load files and scripts.
Also manages project structure.

"""

__all__ = ["Behaviour", "Texture2D", "Prefab",
           "File", "Project", "Skybox", "Scripts"]

from .values import Material, Color
from .core import Component, ShowInInspector
from . import Logger
from OpenGL import GL as gl
from PIL import Image
from types import ModuleType
from uuid import uuid4
import glob
import os
import sys
import ctypes

[docs]def convert(type, list): """ Converts a Python array to a C type from ``ctypes``. Parameters ---------- type : _ctypes.PyCSimpleType Type to cast to. list : list List to cast Returns ------- object A C array """ return (type * len(list))(*list)
[docs]class Behaviour(Component): """ Base class for behaviours that can be scripted. Attributes ---------- gameObject : GameObject GameObject that the component belongs to. transform : Transform Transform that the component belongs to. """ _script = ShowInInspector(type)
[docs] def Start(self): """ Called every time a scene is loaded up. """ pass
[docs] def Update(self, dt): """ Called every frame. Parameters ---------- dt : float Time since last frame, sent by the scene that the Behaviour is in. """ pass
[docs] def FixedUpdate(self, dt): """ Called every frame, in each physics step. Parameters ---------- dt : float Length of this physics step """ pass
[docs] def LateUpdate(self, dt): """ Called every frame, after physics processing. Parameters ---------- dt : float Time since last frame, sent by the scene that the Behaviour is in. """ pass
[docs]class Scripts: """Utility class for loading scripts in a folder.""" var = {}
[docs] @staticmethod def CheckScript(text): """ Check if ``text`` is a valid script for PyUnity. Parameters ---------- text : list List of lines Returns ------- bool If script is valid or not. Notes ----- This function checks each line to see if it matches at least one of these criteria: 1. The line is an ``import`` statement 2. The line is just whitespace or blank 3. The line is just a comment preceded by whitespace or nothing 4. The line is a class definition 5. The line has an indentation at the beginning These checks are essential to ensure no malicious code is run to break the PyUnity engine. """ for line in text: if line.startswith("import") or \ (line.startswith("from") and " import " in line): continue elif line.isspace() or line == "": continue elif "#" in line: if line.split("#")[0].isspace(): continue elif line.startswith("class "): continue elif line.startswith(" ") or line.startswith("\t"): continue return False return True
[docs] @staticmethod def LoadScripts(path): """ Loads all scripts found in ``path``. Parameters ---------- path : Pathlike A path to a folder containing all the scripts Returns ------- ModuleType A module that contains all the imported scripts Notes ----- This function will add a module to ``sys.modules`` that is called ``PyUnityScripts``, and can be imported like any other module. The module will also have a variable called ``__pyunity__`` which shows that it is from PyUnity and not a real module. If an existing module named ``PyUnityScripts`` is present and does not have the ``__pyunity__`` variable set, then a warning will be issued and it will be replaced. """ files = glob.glob(os.path.join(path, "*.py")) if "PyUnityScripts" in sys.modules and hasattr(sys.modules["PyUnityScripts"], "__pyunity__"): module = sys.modules["PyUnityScripts"] else: if "PyUnityScripts" in sys.modules: Logger.LogLine( Logger.WARN, "PyUnityScripts is already a package!") module = ModuleType("PyUnityScripts", None) module.__pyunity__ = True module.__all__ = [] sys.modules["PyUnityScripts"] = module for file in files: with open(file) as f: text = f.read().rstrip().splitlines() name = os.path.basename(file[:-3]) if Scripts.CheckScript(text): c = compile("\n".join(text), name + ".py", "exec") exec(c, Scripts.var) setattr(module, name, Scripts.var[name]) module.__all__.append(name) return module
[docs]class Texture2D: """ Class to represent a texture. """ def __init__(self, path_or_im): if isinstance(path_or_im, str): self.path = path_or_im self.img = Image.open(self.path).convert("RGBA") self.img_data = self.img.tobytes() else: self.path = None self.img = path_or_im self.img_data = self.img.tobytes() self.loaded = False self.texture = None
[docs] def load(self): """ Loads the texture and sets up an OpenGL texture name. """ width, height = self.img.size if self.texture is None: self.texture = gl.glGenTextures(1) gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture) gl.glTexParameterf( gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glTexParameterf( gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, self.img_data) gl.glEnable(gl.GL_TEXTURE_2D) self.loaded = True
[docs] def setImg(self, im): self.loaded = False self.img = im self.path = None self.img_data = self.img.tobytes()
[docs] def use(self): """ Binds the texture for usage. The texture is reloaded if it hasn't already been. """ if not self.loaded: self.load() gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
[docs]class Skybox: """Skybox model consisting of 6 images""" names = ["right.jpg", "left.jpg", "top.jpg", "bottom.jpg", "front.jpg", "back.jpg"] points = [ -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1 ] def __init__(self, path): self.path = path self.compiled = False self.images = []
[docs] def compile(self): self.texture = gl.glGenTextures(1) gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, self.texture) loaded = len(self.images) for i, name in enumerate(Skybox.names): if loaded: img = self.images[i] else: img_path = os.path.join(self.path, name) img = Image.open(img_path).convert("RGBA") self.images.append(img) img_data = img.tobytes() width, height = img.size gl.glTexImage2D(gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, img_data) gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE) self.vbo = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, len(Skybox.points) * gl.sizeof(ctypes.c_float), convert(ctypes.c_float, Skybox.points), gl.GL_STATIC_DRAW) self.vao = gl.glGenVertexArrays(1) gl.glBindVertexArray(self.vao) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer( 0, 3, gl.GL_FLOAT, gl.GL_FALSE, 3 * gl.sizeof(ctypes.c_float), None) Logger.LogLine(Logger.INFO, "Loaded skybox") self.compiled = True
[docs] def use(self): if not self.compiled: self.compile() gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, self.texture)
[docs]class Prefab: """Prefab model""" def __init__(self, gameObject, components): self.gameObject = gameObject self.components = components
[docs]class File: def __init__(self, path, type, uuid=None): self.path = path self.type = type if uuid is None: self.uuid = str(uuid4()) else: self.uuid = uuid self.obj = None
[docs]class Project: def __init__(self, path, name): self.path = path self.name = name self.firstScene = 0 self.files = {} self.file_paths = {}
[docs] def import_file(self, localPath, type, uuid=None): file = File(localPath, type, uuid) self.files[file.uuid] = (file, localPath) self.file_paths[localPath] = file return file
[docs] def reimport_file(self, localPath): old = self.file_paths[localPath] file = File(localPath, old.type, old.uuid) self.files[file.uuid] = (file, localPath) self.file_paths[localPath] = file return file
[docs] def get_file_obj(self, uuid): return self.files[uuid][0].obj
[docs] def write_project(self): with open(os.path.join(self.path, self.name + ".pyunity"), "w+") as f: f.write(f"Project\n" f" name: {self.name}\n" f" firstScene: {self.firstScene}\n" f"Files\n") for uuid, file in sorted(self.files.items(), key=lambda x: x[1][1]): path = os.path.normpath(file[1]).replace("\\", '/') f.write(f" {uuid}: {path}\n") with open(os.path.join(self.path, "__init__.py"), "w+") as f: f.write("from pyunity import *\n") f.write("import os\n\n") f.write( "project = Loader.LoadProject(os.path.abspath(os.path.dirname(__file__)))\n") f.write("firstScene = SceneManager.GetSceneByIndex(project.firstScene)\n") with open(os.path.join(self.path, "__main__.py"), "w+") as f: f.write("from pyunity import *\n") f.write("from . import firstScene\n\n") f.write("SceneManager.LoadScene(firstScene)\n")
[docs] @staticmethod def from_folder(filePath): if not os.path.isdir(filePath): raise ValueError("The specified folder does not exist") files = glob.glob(os.path.join(filePath, "*.pyunity")) if len(files) == 0: raise ValueError("The specified folder is not a PyUnity project") elif len(files) > 1: raise ValueError("Ambiguity in specified folder: " + str(len(files)) + " project files found") file = files[0] with open(file) as f: lines = f.read().rstrip().splitlines() data = {} lines.pop(0) for line in lines: if not line.startswith(" "): break name, value = line[4:].split(": ") data[name] = value data["files"] = {} lines = lines[lines.index("Files") + 1:] for line in lines: name, value = line[4:].split(": ") data["files"][name] = os.path.normpath(value) project = Project(filePath, data["name"]) for uuid, path in data["files"].items(): type_ = os.path.splitext(path)[1][1:].capitalize() if type_ == "Py": type_ = "Behaviour" elif type_ == "Mat": type_ = "Material" project.import_file(path, type_, uuid) project.firstScene = int(data["firstScene"]) return project
[docs] def save_mat(self, mat, name): directory = os.path.join(self.path, "Materials") os.makedirs(directory, exist_ok=True) if mat.texture is not None: if os.path.join("Textures", name + ".png") in self.file_paths: uuid = self.file_paths[os.path.join( "Textures", name + ".png")].uuid else: path = os.path.join(self.path, "Textures", name + ".png") os.makedirs(os.path.dirname(path), exist_ok=True) mat.texture.img.save(path) uuid = self.import_file(path, "Texture2D").uuid else: uuid = "None" with open(os.path.join(directory, name + ".mat"), "w+") as f: f.write("Material\n") f.write(f" albedoColor: {mat.color.to_string()}\n" f" albedoTexture: {uuid}\n")
[docs] def load_mat(self, file): with open(os.path.join(self.path, file.path)) as f: lines = f.read().rstrip().splitlines() lines.pop(0) data = {} for line in lines: name, value = line[4:].split(": ") data[name] = value color = Color.from_string(data["albedoColor"]) material = Material(color) if "albedoTexture" in data and data["albedoTexture"] != "None": uuid = data["albedoTexture"] if self.files[uuid].obj != "None": self.files[uuid].obj = Texture2D( os.path.join(self.path, self.files[uuid].path)) material.texture = self.files[uuid].obj return material