# This file is part of Korman. # # Korman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Korman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Korman. If not, see . from __future__ import annotations import bpy from PyHSPlasma import * from typing import * import weakref from . import explosions from ..korlib import replace_python2_identifier from ..plasma_magic import * # These objects have to be in the plSceneNode pool in order to be loaded... # NOTE: We are using Factory indices because I doubt all of these classes are implemented. _pool_types = ( plFactory.ClassIndex("plPostEffectMod"), plFactory.ClassIndex("pfGUIDialogMod"), plFactory.ClassIndex("plMsgForwarder"), plFactory.ClassIndex("plClothingItem"), plFactory.ClassIndex("plArmatureEffectFootSound"), plFactory.ClassIndex("plDynaFootMgr"), plFactory.ClassIndex("plDynaRippleMgr"), plFactory.ClassIndex("plDynaBulletMgr"), plFactory.ClassIndex("plDynaPuddleMgr"), plFactory.ClassIndex("plATCAnim"), plFactory.ClassIndex("plEmoteAnim"), plFactory.ClassIndex("plDynaRippleVSMgr"), plFactory.ClassIndex("plDynaTorpedoMgr"), plFactory.ClassIndex("plDynaTorpedoVSMgr"), plFactory.ClassIndex("plClusterGroup"), ) class ExportManager: """Friendly resource-managing helper class.""" def __init__(self, exporter): self._exporter = weakref.ref(exporter) self.mgr = plResManager() self.mgr.setVer(globals()[exporter._op.version]) self._nodes = {} self._pages = {} self._keys = {} # cheap inheritance for i in dir(self.mgr): if not hasattr(self, i): setattr(self, i, getattr(self.mgr, i)) def AddAge(self, info): # There's only ever one age being exported, so hook the call and hang onto the plAgeInfo # We'll save from our reference later. Forget grabbing this from C++ self._age_info = info self.mgr.AddAge(info) def AddObject(self, location, obj): """Overloads the plResManager AddObject so we can insert the object into our hashtable""" key = obj.key self._keys[(location, obj.__class__, key.name)] = key self.mgr.AddObject(location, obj) def add_object(self, pl, name=None, bl=None, loc=None, so=None): """Automates adding a converted Blender object to our Plasma Resource Manager""" if loc is not None: location = loc elif so is not None: location = so.key.location elif bl is not None: page_name = bl.plasma_object.page location = self._pages.get(page_name) # location can be None if we've been passed an object in a disabled page, or, in some # cases, a disabled object in the default page. If we got this far, someone is demanding # that this object be created, so we need its page to exist, no matter what the user # otherwise requested. if location is None: location = self._force_create_page(page_name, bl.name) else: raise ValueError("bl, so, or loc must be specified.") # pl can be a class or an instance. # This is one of those "sanity" things to ensure we don't suddenly startpassing around the # key of an uninitialized object. if isinstance(pl, type(object)): assert name or bl or so if name is None: if bl is not None: name = bl.name else: name = so.key.name assert issubclass(pl, hsKeyedObject) pl = pl(self._fixup_key_name(pl, name)) self.AddObject(location, pl) node = self._nodes[location] if node: # All objects must be in the scene node if isinstance(pl, plSceneObject): node.addSceneObject(pl.key) pl.sceneNode = node.key elif pl.ClassIndex() in _pool_types: node.addPoolObject(pl.key) if so is None and isinstance(pl, (plObjInterface, plModifier)): assert bl # Don't use find_create_object because we want to specify the location... Also, we're # in a modifier, so the name we're given might be for a modifier, not the SO key = self.find_key(plSceneObject, bl) if key is None: so = self.add_object(plSceneObject, bl=bl, loc=location) else: so = key.object if isinstance(pl, plObjInterface): pl.owner = so.key # The things I do to make life easy... This is something of a God function now. if isinstance(pl, plAudioInterface): so.audio = pl.key elif isinstance(pl, plCoordinateInterface): so.coord = pl.key elif isinstance(pl, plDrawInterface): so.draw = pl.key elif isinstance(pl, plSimulationInterface): so.sim = pl.key elif pl.ClassIndex() not in _pool_types: so.addInterface(pl.key) elif isinstance(pl, plModifier) and pl.ClassIndex() not in _pool_types: so.addModifier(pl.key) # And we're done! return pl def create_builtins(self, age, textures): # WARNING: Path of the Shell expects for all builtin pages to be at bare minimum # present and valid. They do not have to have any contents. See AvatarCustomization. # BuiltIn.prp want_pysdl = bpy.context.scene.world.plasma_age.age_sdl builtin = self.create_page(age, "BuiltIn", -2, builtin=True) if want_pysdl: self._pack_agesdl_hook(age) sdl = self.add_object(plSceneObject, name="AgeSDLHook", loc=builtin) pfm = self.add_object(plPythonFileMod, name="VeryVerySpecialPythonFileMod", so=sdl) pfm.filename = replace_python2_identifier(age) # Textures.prp # FIXME: unconditional creation will overwrite any existing textures PRP. This should # be addressed by a successful implementation of #145. self.create_page(age, "Textures", -1, builtin=True) def create_page(self, age, name, id, *, builtin=False, external=False): location = plLocation(self.mgr.getVer()) location.prefix = bpy.context.scene.world.plasma_age.seq_prefix if builtin: location.flags |= plLocation.kBuiltIn location.page = id if not external: self._pages[name] = location # If the page ID is 0, this is the default page... Any page with an empty string name # is the default, so bookmark it! if id == 0: assert not external, "The default page cannot be external!" self._pages[""] = location if not external: info = plPageInfo() info.age = age info.page = name info.location = location self.mgr.AddPage(info) if not builtin: self._age_info.addPage((name, id, 0)) if not external: if self.getVer() <= pvPots: node = plSceneNode(f"{age}_District_{name}") else: node = plSceneNode(f"{age}_{name}") self.mgr.AddObject(location, node) else: node = None else: node = None self._nodes[location] = node return location @property def _encryption(self): if self.mgr.getVer() == pvEoa: return plEncryptedStream.kEncAes else: return plEncryptedStream.kEncXtea def find_interfaces(self, pClass, so : plSceneObject) -> Iterable[plObjInterface]: assert issubclass(pClass, plObjInterface) for i in (i.object for i in so.interfaces): if isinstance(i, pClass): yield i def find_create_key(self, pClass: Type[KeyedT], bl=None, name=None, so=None) -> plKey[KeyedT]: key = self.find_key(pClass, bl, name, so) if key is None: key = self.add_object(pl=pClass, name=name, bl=bl, so=so).key return key def find_key(self, pClass: Type[KeyedT], bl=None, name=None, so=None, loc=None) -> Optional[plKey[KeyedT]]: """Given a blender Object and a Plasma class, find (or create) an exported plKey""" if loc is not None: location = loc elif so is not None: location = so.key.location elif bl is not None: location = self._pages.get(bl.plasma_object.page) if location is None: # This page has never been created, so the key search will obviously fail. return None else: raise ValueError("bl, so, or loc must be specified.") if name is None: if bl is not None: name = bl.name else: name = so.key.name name = self._fixup_key_name(pClass, name) key = self._keys.get((location, pClass, name), None) if key is not None and so is not None: # Purposefully not checking for plObjInterface -- they should never be shared. if issubclass(pClass, plModifier): if key not in so.modifiers: # We really shouldn't add plSingleModifiers to multiple objects. This may # potentially cause URU to crash. I'm uncertain though, so we'll just warn # for now. if issubclass(pClass, plSingleModifier): self._exporter().report.warn("Adding SingleModifier '{}' (type: '{}'') to another SceneObject '{}'", key.name, pClass.__name__[2:], so.key.name) so.addModifier(key) return key def find_create_object( self, pClass: Type[KeyedT], bl: bpy.types.Object = None, name: Optional[str] = None, so: Optional[plSceneObject] = None ) -> KeyedT: key = self.find_key(pClass, bl, name, so) if key is None: return self.add_object(pl=pClass, name=name, bl=bl, so=so) return key.object def find_object(self, pClass: Type[KeyedT], bl=None, name=None, so=None) -> Optional[KeyedT]: key = self.find_key(pClass, bl, name, so) if key is not None: return key.object return None def _fixup_key_name(self, pClass, name): if pClass in {plSceneObject, plPythonFileMod}: return replace_python2_identifier(name) return name def _force_create_page(self, page_name: str, requestor_name: str) -> plLocation: """Forcibly creates a page (that you KNOW does not exist yet) during the convert process.""" age_name = self._exporter().age_name if page_name: # This page has a name, but hasn't yet been created. self._exporter().report.warn( "'{}': trying to export into an unknown, potentially disabled page '{}'. Attempting to create it.", requestor_name, page_name ) age_info = bpy.context.scene.world.plasma_age page_info = next((i for i in age_info.pages if i.name == page_name), None) if page_info is None: error = explosions.UndefinedPageError() error.add(page_name, requestor_name) error.raise_if_error() location = self.create_page(age_name, page_name, page_info.id) else: # This is a default page that wasn't exported... for some reason... self._exporter().report.warn( "'{}': trying to export into the default page, but it seems to not exist. Attempting to create it.", requestor_name ) # See if a default page exists by name. That means that it doesn't have ID = 0. location = next((j for i, j in self._pages.items() if i.lower() == "default"), None) if location is None: # OK, we can be fairly certain that page ID 0 and page name "Default" don't exist. location = self.create_page(age_name, "Default", 0) else: # Avoid doing this stupid lookup in the future. self._pages[""] = location return location def get_location(self, bl) -> plLocation: """Returns the Page Location of a given Blender Object""" return self._pages[bl.plasma_object.page] def get_scene_node( self, location: Optional[plLocation] = None, bl: Optional[bpy.types.Object]=None ) -> plKey[plSceneNode]: """Gets a Plasma Page's plSceneNode key""" assert (location is not None) ^ (bl is not None) if location is None: location = self._pages[bl.plasma_object.page] return self._nodes[location].key def get_textures_page(self, key: plKey) -> plLocation: """Gets the appropriate page for a texture for a given plLayer""" # The point of this is to account for per-page textures... if bpy.context.scene.world.plasma_age.use_texture_page: return self._pages["Textures"] else: return key.location def _pack_agesdl_hook(self, age): output = self._exporter().output def get_text(name): namei = name.lower() generator = (i for i in bpy.data.texts if i.name.lower() == namei) result, collision = next(generator, None), next(generator, None) if collision is not None: raise explosions.ExportError("There are multiple copies of case insensitive text block '{}'.", name) return result # AgeSDL Hook Python fixed_agename = replace_python2_identifier(age) py_filename = "{}.py".format(fixed_agename) age_py = get_text(py_filename) if output.want_py_text(age_py): py_code = age_py.as_string() else: py_code = very_very_special_python.format(age_name=fixed_agename).lstrip() output.add_python_mod(py_filename, text_id=age_py, str_data=py_code) # AgeSDL sdl_filename = "{}.sdl".format(fixed_agename) age_sdl = get_text(sdl_filename) if age_sdl is None: sdl_code = very_very_special_sdl.format(age_name=fixed_agename).lstrip() age_sdl = bpy.data.texts.new(sdl_filename) age_sdl.from_string(sdl_code) output.add_sdl(sdl_filename, text_id=age_sdl) def save_age(self): self._write_age() self._write_fni() self._write_pages() def _write_age(self): f = "{}.age".format(self._age_info.name) output = self._exporter().output with output.generate_dat_file(f, enc=self._encryption) as stream: self._age_info.writeToStream(stream) def _write_fni(self): f = "{}.fni".format(self._age_info.name) output = self._exporter().output with output.generate_dat_file(f, enc=self._encryption) as stream: fni = bpy.context.scene.world.plasma_fni stream.writeLine("Graphics.Renderer.SetClearColor {:.2f} {:.2f} {:.2f}".format(*fni.clear_color)) stream.writeLine("Graphics.Renderer.SetYon {:.1f}".format(fni.yon)) if fni.fog_method == "none": stream.writeLine("Graphics.Renderer.Fog.SetDefLinear 0 0 0") else: stream.writeLine("Graphics.Renderer.Fog.SetDefColor {:.2f} {:.2f} {:.2f}".format(*fni.fog_color)) if fni.fog_method == "linear": stream.writeLine("Graphics.Renderer.Fog.SetDefLinear {:.2f} {:.2f} {:.2f}".format(fni.fog_start, fni.fog_end, fni.fog_density)) elif fni.fog_method == "exp": stream.writeLine("Graphics.Renderer.Fog.SetDefExp {:.2f} {:.2f}".format(fni.fog_end, fni.fog_density)) elif fni.fog_method == "exp2": stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 {:.2f} {:.2f}".format(fni.fog_end, fni.fog_density)) def _write_pages(self): age_name = self._age_info.name output = self._exporter().output for loc in self._pages.values(): page = self.mgr.FindPage(loc) # not cached because it's C++ owned chapter = "_District_" if self.mgr.getVer() <= pvMoul else "_" f = "{}{}{}.prp".format(age_name, chapter, page.page) with output.generate_dat_file(f) as stream: self.mgr.WritePage(stream, page)