|
| 1 | +''' |
| 2 | +Code for creating and using subclassses of C++ and Blueprint classes |
| 3 | +
|
| 4 | +The general starting point and goals we use for subclassing are: |
| 5 | +- you have some existing UClass in your project (either a Blueprint class or a C++ class) that has an arbitrarily deep class |
| 6 | + ancestry, and you want to create a Python subclass of it |
| 7 | +- in Python you want to be able to subclass further as needed |
| 8 | +- as much as possible, Python subclasses should be first-class citizens in UE4 - they should be usable anywhere that Blueprint |
| 9 | + classes and objects are usable |
| 10 | +- at the same time, Python subclasses and objects from them should be as Pythonic as possible |
| 11 | +''' |
| 12 | +from . common import * |
| 13 | +import _fmsubclassing as fms |
| 14 | +from unreal_engine import classes as engine_classes |
| 15 | + |
| 16 | +# decorator that is roughly equivalent to UFUNCTION() in C++ - use this on a method in a subclass to cause it to |
| 17 | +# be exposed as callable |
| 18 | +# TODO: add support for pure=True, etc. |
| 19 | +def ufunction(f): |
| 20 | + f.ufunction = True |
| 21 | + return f |
| 22 | + |
| 23 | +# used for declaring UPROPERTYs. Use when creating class vars: myVar = uproperty(FVector, FVector(1,2,3)). By default, implies BlueprintReadWrite. |
| 24 | +# TODO: add support for replication, editanywhere, BPReadOnly, repnotify, and other flags. myVar = uproperty(default, *kwFlags) |
| 25 | +class uproperty: |
| 26 | + def __init__(self, type, default, is_class=False): # TODO: make default optional, but find a suitable default (e.g. int=0, bool=False, etc. Or, let them give a default and we infer the type in many cases |
| 27 | + self.type = type |
| 28 | + self.default = default |
| 29 | + self.is_class = is_class # i.e. the property holds a UClass not a UObject |
| 30 | + |
| 31 | +def CombinedPropertyDefaults(cls): |
| 32 | + '''Recursively collects all uproperty defaults for this and all parent python classes''' |
| 33 | + props = {} |
| 34 | + for baseClass in cls.__bases__[::-1]: # go farthest back first |
| 35 | + props.update(CombinedPropertyDefaults(baseClass)) |
| 36 | + |
| 37 | + # Now fold in our defaults |
| 38 | + props.update(getattr(cls, '__property_defaults__', {})) |
| 39 | + return props |
| 40 | + |
| 41 | +class MetaBase(type): |
| 42 | + '''Metaclass used to help in the creation of Python subclasses of engine classes''' |
| 43 | + def __new__(metaclass, name, bases, dct): |
| 44 | + # remove any uproperties before creating the class - they get processed later |
| 45 | + uprops = [] # (name, uprop obj) |
| 46 | + for k,v in list(dct.items()): |
| 47 | + if isinstance(v, uproperty): |
| 48 | + dct.pop(k) |
| 49 | + uprops.append((k,v)) |
| 50 | + |
| 51 | + interfaces = [] |
| 52 | + for cls in dct.get('__interfaces__', []): |
| 53 | + assert isinstance(cls, UObject), '%r cannot be used as an interface class' % cls |
| 54 | + assert cls.class_get_flags() & ue.CLASS_INTERFACE, '%r is not an interface class' % cls |
| 55 | + interfaces.append(cls) |
| 56 | + |
| 57 | + newPyClass = super().__new__(metaclass, name, bases, dct) |
| 58 | + |
| 59 | + # TODO: add some checks to bases to verify that there is at most 1 engine class present in the ancestry |
| 60 | + # TODO: add some checks to make sure that if __uclass__ is present, it exactly matches the ancestor one (ti's unneeded but harmless) |
| 61 | + # (eh, I think we should raise an error if it's present) |
| 62 | + dct['get_py_proxy'] = lambda self:fms.get_py_proxy(self) |
| 63 | + |
| 64 | + if name == 'BridgeBase': |
| 65 | + pass # No extra processing for this case |
| 66 | + else: |
| 67 | + assert len(bases) > 0, 'This class must subclass something else' |
| 68 | + assert issubclass(newPyClass, BridgeBase), 'MetaBase is only for use with subclassing BridgeBase' |
| 69 | + isBridge = bases[0] == BridgeBase # is this a bridge class or some further descendent? |
| 70 | + if isBridge: |
| 71 | + engineParentClass = getattr(newPyClass, '__uclass__', None) |
| 72 | + assert engineParentClass is not None, 'Missing __uclass__ property' |
| 73 | + else: |
| 74 | + engineParentClass = bases[0].engineClass |
| 75 | + |
| 76 | + # create a new corresponding UClass and set up for UPROPERTY handling |
| 77 | + newPyClass.engineClass = fms.create_subclass(name, engineParentClass, newPyClass) |
| 78 | + metaclass.SetupProperties(newPyClass, engineParentClass, uprops) |
| 79 | + |
| 80 | + # add in any interfaces this class claims to support (TODO: verify that all necessary methods are implemented) |
| 81 | + for cls in interfaces: |
| 82 | + fms.add_interface(newPyClass.engineClass, cls) |
| 83 | + |
| 84 | + # Scan the class and process its methods to wire things up with UE4 |
| 85 | + if isBridge: |
| 86 | + metaclass.ProcessBridgeClassMethods(newPyClass, engineParentClass) |
| 87 | + else: |
| 88 | + metaclass.ProcessBridgeDescendentClassMethods(newPyClass) |
| 89 | + |
| 90 | + return newPyClass |
| 91 | + |
| 92 | + @classmethod |
| 93 | + def SetupProperties(metaclass, newPyClass, engineParentClass, upropList): |
| 94 | + '''Called from __new__ to get set up for UPROPERTY handling - saves list of property names and registers with the |
| 95 | + UClass any properties added by this Python class''' |
| 96 | + # Store a list of all known UPROPERTYs - most of property handling is deferred until they are used so at this point |
| 97 | + # we just make a note of their names. Due to FNames being case-insensitive, we need to be case-insensitive on the Python |
| 98 | + # side for the most part too. |
| 99 | + newPyClass.__property_names__ = [x.lower() for x in fms.get_uproperty_names(engineParentClass)] |
| 100 | + |
| 101 | + initialValues = {} # prop name --> default value |
| 102 | + for origK,v in upropList: |
| 103 | + k = origK.lower() |
| 104 | + if not isinstance(v, uproperty): |
| 105 | + continue |
| 106 | + initialValues[k] = v.default # these will be set during init |
| 107 | + |
| 108 | + if k not in newPyClass.__property_names__: |
| 109 | + # This is not a known property, so we need to register it with the UClass. Use the original case to try to |
| 110 | + # make it look nice, but it's anybody's guess because if anywhere else in the system an FName of the same name |
| 111 | + # has been created already, its capitalization is what will be used (this matters because UE4 likes to take names |
| 112 | + # like 'MyIntProp' and display them as 'My Int Prop'). |
| 113 | + newPyClass.__property_names__.append(k) |
| 114 | + fms.add_uproperty(newPyClass.engineClass, origK, v.type, v.is_class) |
| 115 | + |
| 116 | + newPyClass.__property_defaults__ = initialValues |
| 117 | + |
| 118 | + @classmethod |
| 119 | + def ProcessBridgeClassMethods(metaclass, newPyClass, engineParentClass): |
| 120 | + '''Called from __new__ to handle the case where the class being created is a bridge class. For each UFUNCTION in the class ancestry, |
| 121 | + adds a corresponding Python callable to that UFUNCTION that results in a call to the C++ implementation (effectively creating the super() |
| 122 | + implementation for each UFUNCTION).''' |
| 123 | + for funcName in fms.get_ufunction_names(engineParentClass): |
| 124 | + # Make it so that from Python you can use that name to call the UFUNCTION version in UE4 |
| 125 | + metaclass.AddMethodCaller(engineParentClass, newPyClass, funcName) |
| 126 | + |
| 127 | + @classmethod |
| 128 | + def ProcessBridgeDescendentClassMethods(metaclass, newPyClass): |
| 129 | + '''Called from __new__ to handle the case where the class being created is a descendent of a bridge class. For each method that is marked |
| 130 | + as a ufunction, adds that method to the class but under a different name (_orig_<funcName>), adds a UFUNCTION to the new class on the C++ |
| 131 | + side to call that method, and adds a Python callable to that UFUNCTION (*not* to the method directly, so that calls like self.Foo will |
| 132 | + go through the UE4 layer for things like replication).''' |
| 133 | + for k,v in list(newPyClass.__dict__.items()): |
| 134 | + if k.startswith('__') or k in ('engineClass',): |
| 135 | + continue |
| 136 | + if not callable(v) or not getattr(v, 'ufunction', None): |
| 137 | + continue |
| 138 | + funcName, func = k,v |
| 139 | + |
| 140 | + # Add the method to the class but under a different name so it doesn't get stomped by the stuff below |
| 141 | + hiddenFuncName = '_orig_' + funcName |
| 142 | + setattr(newPyClass, hiddenFuncName, func) |
| 143 | + |
| 144 | + # Expose a UFUNCTION in C++ that calls this method |
| 145 | + fms.add_ufunction(newPyClass.engineClass, funcName, func) |
| 146 | + |
| 147 | + # Make it so that from Python you can use that name to call the UFUNCTION version in UE4 (so that things |
| 148 | + # like replication work) |
| 149 | + metaclass.AddMethodCaller(newPyClass.engineClass, newPyClass, funcName) |
| 150 | + |
| 151 | + # TODO: somewhere in here we should warn if the user tries to override something that is BPCallable but not BPNativeEvent |
| 152 | + # and not BPImeplementableEvent - the engine will allow us but the the results won't work quite right |
| 153 | + |
| 154 | + @classmethod |
| 155 | + def AddMethodCaller(metaclass, classWithFunction, newPyClass, funcName): |
| 156 | + '''Adds to newPyClass a method that calls the UFUNCTION C++ method of the same name. classWithFunction is the engine class |
| 157 | + that has that function (we can't do a dynamic lookup by name later because then super() calls don't work - we have to bind |
| 158 | + to the function on the class now).''' |
| 159 | + uFunc = fms.get_ufunction_object(classWithFunction, funcName) |
| 160 | + def _(self, *args, **kwargs): |
| 161 | + return fms.call_ufunction_object(self._instAddr, self, uFunc, args, kwargs) |
| 162 | + _.__name__ = funcName |
| 163 | + setattr(newPyClass, funcName, _) |
| 164 | + |
| 165 | +class BridgeBase: #(metaclass=MetaBase): - let the metaclass be specified dynamically |
| 166 | + '''Base class of all bridge classes we generate''' |
| 167 | + def __new__(cls, instAddr, *args, **kwargs): # same convention as with __init__ |
| 168 | + inst = super(BridgeBase, cls).__new__(cls) |
| 169 | + |
| 170 | + # Set any default property values - we gather the defaults up from parent classes too, although it seems like those |
| 171 | + # should happen automatically via the CDO, no? |
| 172 | + for propName, default in CombinedPropertyDefaults(cls).items(): |
| 173 | + try: |
| 174 | + fms.set_uproperty_value(instAddr, propName, default, False) |
| 175 | + except: |
| 176 | + logTB() |
| 177 | + return inst |
| 178 | + |
| 179 | + def __init__(self, instAddr): |
| 180 | + self._instAddr = instAddr # by convention, the UObject addr is passed to the instance |
| 181 | + |
| 182 | + @property |
| 183 | + def uobject(self): |
| 184 | + '''gets the UObject that owns self, wrapped as a python object so it can be passed to other APIs''' |
| 185 | + return fms.get_ue_inst(self._instAddr) |
| 186 | + |
| 187 | + def __setattr__(self, origK, v): |
| 188 | + k = origK.lower() |
| 189 | + if k in self.__class__.__property_names__: |
| 190 | + fms.set_uproperty_value(self._instAddr, k, v, True) |
| 191 | + else: |
| 192 | + self.__dict__[origK] = v |
| 193 | + |
| 194 | + def __getattr__(self, origK): |
| 195 | + k = origK.lower() |
| 196 | + if k in self.__class__.__property_names__: |
| 197 | + return fms.get_uproperty_value(self._instAddr, k) |
| 198 | + else: |
| 199 | + try: |
| 200 | + return self.__dict__[origK] |
| 201 | + except KeyError: |
| 202 | + raise AttributeError('No such attribute %r' % origK) |
| 203 | + |
| 204 | +class BridgeClassGenerator: |
| 205 | + '''Dynamically creates the bridge class for any UE4 class''' |
| 206 | + def __init__(self, metaclass): |
| 207 | + self.cache = {} # class name --> class instance |
| 208 | + self.newClassMetaclass = metaclass |
| 209 | + |
| 210 | + def __getattr__(self, className): |
| 211 | + try: |
| 212 | + # return cached copy if we've already generated it previously |
| 213 | + return self.cache[className] |
| 214 | + except KeyError: |
| 215 | + # generate a new class and cache it |
| 216 | + engineClass = getattr(engine_classes, className) # let this raise an error if the class doesn't exist |
| 217 | + |
| 218 | + #from unreal_engine.classes import |
| 219 | + dct = dict( |
| 220 | + __uclass__ = engineClass |
| 221 | + ) |
| 222 | + meta = self.newClassMetaclass |
| 223 | + cls = meta.__new__(meta, className+'___Bridge', (BridgeBase,), dct) |
| 224 | + self.cache[className] = cls |
| 225 | + return cls |
| 226 | + |
| 227 | +# TODO: make this implicit - i.e. let subclasses pass in an unreal_engine.classes class obj and have metaclass wrap them automatically |
| 228 | +bridge = BridgeClassGenerator(MetaBase) |
| 229 | + |
0 commit comments