import P from "parsernostrum" import Configuration from "../Configuration.js" import nodeColor from "../decoding/nodeColor.js" import nodeIcon from "../decoding/nodeIcon.js" import nodeVariadic from "../decoding/nodeVariadic.js" import Grammar from "../serialization/Grammar.js" import Utility from "../Utility.js" import AlternativesEntity from "./AlternativesEntity.js" import ArrayEntity from "./ArrayEntity.js" import BooleanEntity from "./BooleanEntity.js" import FunctionReferenceEntity from "./FunctionReferenceEntity.js" import GuidEntity from "./GuidEntity.js" import IEntity from "./IEntity.js" import IntegerEntity from "./IntegerEntity.js" import LinearColorEntity from "./LinearColorEntity.js" import MacroGraphReferenceEntity from "./MacroGraphReferenceEntity.js" import MirroredEntity from "./MirroredEntity.js" import NaturalNumberEntity from "./NaturalNumberEntity.js" import NullEntity from "./NullEntity.js" import NumberEntity from "./NumberEntity.js" import ObjectReferenceEntity from "./ObjectReferenceEntity.js" import PinEntity from "./PinEntity.js" import ScriptVariableEntity from "./ScriptVariableEntity.js" import StringEntity from "./StringEntity.js" import SymbolEntity from "./SymbolEntity.js" import UnknownPinEntity from "./UnknownPinEntity.js" import VariableReferenceEntity from "./VariableReferenceEntity.js" export default class ObjectEntity extends IEntity { #exported = false get exported() { return this.#exported } set exported(value) { this.#exported = value } static #nameRegex = /^(\w+?)(?:_(\d+))?$/ /** @type {(k: String) => String} */ static printKey = k => !k.startsWith(Configuration.subObjectAttributeNamePrefix) ? k : "" static attributeSeparator = "\n" static wrap = this.notWrapped static trailing = true static attributes = { ...super.attributes, Class: ObjectReferenceEntity, Name: StringEntity, Archetype: ObjectReferenceEntity, ExportPath: MirroredEntity.of(ObjectReferenceEntity), ObjectRef: ObjectReferenceEntity, BlueprintElementType: ObjectReferenceEntity, BlueprintElementInstance: ObjectReferenceEntity, ConstA: MirroredEntity.of(NumberEntity), ConstB: MirroredEntity.of(NumberEntity), PinTags: ArrayEntity.of(NullEntity).flagInlined(), PinNames: ArrayEntity.of(StringEntity).flagInlined(), AxisKey: SymbolEntity, InputAxisKey: SymbolEntity, InputName: StringEntity, InputType: SymbolEntity, NumAdditionalInputs: NaturalNumberEntity, bIsPureFunc: BooleanEntity, bIsConstFunc: BooleanEntity, bIsCaseSensitive: BooleanEntity, bDefaultsToPureFunc: BooleanEntity, VariableReference: VariableReferenceEntity, SelfContextInfo: SymbolEntity, DelegatePropertyName: StringEntity, DelegateOwnerClass: ObjectReferenceEntity, ComponentPropertyName: StringEntity, EventReference: FunctionReferenceEntity, FunctionReference: FunctionReferenceEntity, FunctionScript: ObjectReferenceEntity, CustomFunctionName: StringEntity, TargetType: ObjectReferenceEntity, MacroGraphReference: MacroGraphReferenceEntity, Enum: ObjectReferenceEntity, EnumEntries: ArrayEntity.of(StringEntity).flagInlined(), InputKey: SymbolEntity, OpName: StringEntity, CachedChangeId: GuidEntity, FunctionDisplayName: StringEntity, AddedPins: ArrayEntity.of(UnknownPinEntity).withDefault().flagInlined().flagSilent(), ChangeId: GuidEntity, MaterialFunction: ObjectReferenceEntity, bOverrideFunction: BooleanEntity, bInternalEvent: BooleanEntity, bConsumeInput: BooleanEntity, bExecuteWhenPaused: BooleanEntity, bOverrideParentBinding: BooleanEntity, bControl: BooleanEntity, bAlt: BooleanEntity, bShift: BooleanEntity, bCommand: BooleanEntity, CommentColor: LinearColorEntity, bCommentBubbleVisible_InDetailsPanel: BooleanEntity, bColorCommentBubble: BooleanEntity, ProxyFactoryFunctionName: StringEntity, ProxyFactoryClass: ObjectReferenceEntity, ProxyClass: ObjectReferenceEntity, StructType: ObjectReferenceEntity, MaterialExpression: ObjectReferenceEntity, MaterialExpressionComment: ObjectReferenceEntity, MoveMode: SymbolEntity, TimelineName: StringEntity, TimelineGuid: GuidEntity, SizeX: MirroredEntity.of(IntegerEntity), SizeY: MirroredEntity.of(IntegerEntity), Text: MirroredEntity.of(StringEntity), ParameterName: StringEntity, ExpressionGUID: GuidEntity, MaterialExpressionEditorX: MirroredEntity.of(IntegerEntity), MaterialExpressionEditorY: MirroredEntity.of(IntegerEntity), MaterialExpressionGuid: GuidEntity, NodeTitle: StringEntity, NodeTitleColor: LinearColorEntity, PositionX: MirroredEntity.of(IntegerEntity), PositionY: MirroredEntity.of(IntegerEntity), SettingsInterface: ObjectReferenceEntity, PCGNode: ObjectReferenceEntity, SoundNode: ObjectReferenceEntity, SoundWaveAssetPtr: ObjectReferenceEntity, HiGenGridSize: SymbolEntity, Operation: SymbolEntity, NodePosX: IntegerEntity, NodePosY: IntegerEntity, NodeHeight: IntegerEntity, NodeWidth: IntegerEntity, Graph: ObjectReferenceEntity, SubgraphInstance: StringEntity, InputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(), OutputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(), bExposeToLibrary: BooleanEntity, bCanRenameNode: BooleanEntity, bCommentBubblePinned: BooleanEntity, bCommentBubbleVisible: BooleanEntity, NodeComment: StringEntity, AdvancedPinDisplay: SymbolEntity, DelegateReference: VariableReferenceEntity, EnabledState: SymbolEntity, NodeGuid: GuidEntity, ErrorType: IntegerEntity, ErrorMsg: StringEntity, ScriptVariables: ArrayEntity.flagInlined().of(ScriptVariableEntity), Node: MirroredEntity.of(ObjectReferenceEntity), ExportedNodes: StringEntity, CustomProperties: ArrayEntity .of(AlternativesEntity.accepting(PinEntity, UnknownPinEntity)) .withDefault() .flagSilent(), } static customPropertyGrammar = P.seq( P.reg(/CustomProperties\s+/), this.attributes.CustomProperties.type.grammar, ).map(([_0, pin]) => values => { /** @type {InstanceType} */( values.CustomProperties ??= new (this.attributes.CustomProperties)() ).values.push(pin) }) static inlinedArrayEntryGrammar = P.seq( P.alt( Grammar.symbolQuoted.map(v => [v, true]), Grammar.symbol.map(v => [v, false]), ), P.reg(new RegExp(String.raw`\s*\(\s*(\d+)\s*\)\s*\=\s*`), 1).map(Number) // Number in parentheses then equal ).chain( /** @param {[[keyof ObjectEntity.attributes, Boolean], Number]} param */ ([[symbol, quoted], index]) => (this.attributes[symbol]?.grammar ?? IEntity.unknownEntityGrammar).map(currentValue => values => { if (values[symbol] === undefined) { let arrayEntity = ArrayEntity if (quoted != arrayEntity.quoted) { arrayEntity = arrayEntity.flagQuoted(quoted) } if (!arrayEntity.inlined) { arrayEntity = arrayEntity.flagInlined() } values[symbol] = new arrayEntity() } /** @type {ArrayEntity} */ const target = values[symbol] target.values[index] = currentValue } ) ) static grammar = this.createGrammar() static grammarMultipleObjects = P.seq( P.whitespaceOpt, this.grammar, P.seq( P.whitespace, this.grammar, ) .map(([_0, object]) => object) .many(), P.whitespaceOpt ).map(([_0, first, remaining, _4]) => [first, ...remaining]) constructor(values = {}) { if (("NodePosX" in values) !== ("NodePosY" in values)) { const entries = Object.entries(values) const [key, position] = "NodePosX" in values ? ["NodePosY", Object.keys(values).indexOf("NodePosX") + 1] : ["NodePosX", Object.keys(values).indexOf("NodePosY")] entries.splice(position, 0, [key, new IntegerEntity(0)]) values = Object.fromEntries(entries) } super(values) // Attributes /** @type {ArrayEntity} */ this.CustomProperties /** @type {InstanceType} */ this.AddedPins /** @type {InstanceType} */ this.AdvancedPinDisplay /** @type {InstanceType} */ this.Archetype /** @type {InstanceType} */ this.AxisKey /** @type {InstanceType} */ this.bIsPureFunc /** @type {InstanceType} */ this.bDefaultsToPureFunc /** @type {InstanceType} */ this.BlueprintElementInstance /** @type {InstanceType} */ this.BlueprintElementType /** @type {InstanceType} */ this.Class /** @type {InstanceType} */ this.CommentColor /** @type {InstanceType} */ this.ComponentPropertyName /** @type {InstanceType} */ this.ConstA /** @type {InstanceType} */ this.ConstB /** @type {InstanceType} */ this.CustomFunctionName /** @type {InstanceType} */ this.DelegatePropertyName /** @type {InstanceType} */ this.DelegateReference /** @type {InstanceType} */ this.EnabledState /** @type {InstanceType} */ this.Enum /** @type {InstanceType} */ this.EnumEntries /** @type {InstanceType} */ this.EventReference /** @type {InstanceType} */ this.ExportedNodes /** @type {InstanceType} */ this.ExportPath /** @type {InstanceType} */ this.FunctionDisplayName /** @type {InstanceType} */ this.FunctionReference /** @type {InstanceType} */ this.FunctionScript /** @type {InstanceType} */ this.Graph /** @type {InstanceType} */ this.HiGenGridSize /** @type {InstanceType} */ this.InputAxisKey /** @type {InstanceType} */ this.InputKey /** @type {InstanceType} */ this.InputName /** @type {InstanceType} */ this.InputPins /** @type {InstanceType} */ this.InputType /** @type {InstanceType} */ this.MacroGraphReference /** @type {InstanceType} */ this.MaterialExpression /** @type {InstanceType} */ this.MaterialExpressionComment /** @type {InstanceType} */ this.MaterialExpressionEditorX /** @type {InstanceType} */ this.MaterialExpressionEditorY /** @type {InstanceType} */ this.MaterialFunction /** @type {InstanceType} */ this.Name /** @type {InstanceType} */ this.Node /** @type {InstanceType} */ this.NodeComment /** @type {InstanceType} */ this.NodeHeight /** @type {InstanceType} */ this.NodePosX /** @type {InstanceType} */ this.NodePosY /** @type {InstanceType} */ this.NodeTitle /** @type {InstanceType} */ this.NodeTitleColor /** @type {InstanceType} */ this.NodeWidth /** @type {InstanceType} */ this.NumAdditionalInputs /** @type {InstanceType} */ this.ObjectRef /** @type {InstanceType} */ this.Operation /** @type {InstanceType} */ this.OpName /** @type {InstanceType} */ this.OutputPins /** @type {InstanceType} */ this.ParameterName /** @type {InstanceType} */ this.PCGNode /** @type {InstanceType} */ this.SoundNode /** @type {InstanceType} */ this.SoundWaveAssetPtr /** @type {InstanceType} */ this.PinNames /** @type {InstanceType} */ this.PinTags /** @type {InstanceType} */ this.PositionX /** @type {InstanceType} */ this.PositionY /** @type {InstanceType} */ this.ProxyFactoryFunctionName /** @type {InstanceType} */ this.ScriptVariables /** @type {InstanceType} */ this.SettingsInterface /** @type {InstanceType} */ this.SizeX /** @type {InstanceType} */ this.SizeY /** @type {InstanceType} */ this.StructType /** @type {InstanceType} */ this.SubgraphInstance /** @type {InstanceType} */ this.TargetType /** @type {InstanceType} */ this.Text /** @type {InstanceType} */ this.Text /** @type {InstanceType} */ this.VariableReference // Legacy nodes pins if (this["Pins"] instanceof ArrayEntity) { this["Pins"].valueOf().forEach( /** @param {ObjectReferenceEntity} objectReference */ objectReference => { const pinObject = this[Configuration.subObjectAttributeNameFromReference(objectReference, true)] if (pinObject) { const pinEntity = PinEntity.fromLegacyObject(pinObject) pinEntity.LinkedTo = new (PinEntity.attributes.LinkedTo)() this.getCustomproperties(true).push(pinEntity) this.CustomProperties.ignored = true } } ) } /** @type {ObjectEntity} */ const materialSubobject = this.getMaterialSubobject() if (materialSubobject) { const obj = materialSubobject obj.SizeX !== undefined && (obj.SizeX.getter = () => this.NodeWidth) obj.SizeY && (obj.SizeY.getter = () => this.NodeHeight) obj.Text && (obj.Text.getter = () => this.NodeComment) obj.MaterialExpressionEditorX && (obj.MaterialExpressionEditorX.getter = () => this.NodePosX) obj.MaterialExpressionEditorY && (obj.MaterialExpressionEditorY.getter = () => this.NodePosY) if (this.getType() === Configuration.paths.materialExpressionComponentMask) { const rgbaPins = Configuration.rgba.map(pinName => { const result = this.getPinEntities().find(pin => pin.PinName.toString() === pinName) result.recomputesNodeTitleOnChange = true return result }) // Reorder keys so that the added ones stay first obj.keys = [...Configuration.rgba, ...obj.keys] const silentBool = MirroredEntity.of(BooleanEntity).withDefault().flagSilent() obj["R"] = new silentBool(() => rgbaPins[0].DefaultValue) obj["G"] = new silentBool(() => rgbaPins[1].DefaultValue) obj["B"] = new silentBool(() => rgbaPins[2].DefaultValue) obj["A"] = new silentBool(() => rgbaPins[3].DefaultValue) } else if (this.getType() === Configuration.paths.materialExpressionSubtract) { const silentNumber = MirroredEntity .of(NumberEntity.withPrecision(6)) .withDefault(() => new MirroredEntity(() => new NumberEntity(1))) .flagSilent() const pinA = this.getCustomproperties().find(pin => pin.PinName?.toString() === "A") const pinB = this.getCustomproperties().find(pin => pin.PinName?.toString() === "B") if (pinA || pinB) { // Reorder keys so that the added ones stay first obj.keys = ["ConstA", "ConstB", ...obj.keys] if (pinA) { pinA.recomputesNodeTitleOnChange = true obj.ConstA = new silentNumber(() => pinA.DefaultValue) } if (pinB) { pinB.recomputesNodeTitleOnChange = true obj.ConstB = new silentNumber(() => pinB.DefaultValue) } } } } /** @type {ObjectEntity} */ const pcgObject = this.getPcgSubobject() if (pcgObject) { pcgObject.PositionX && (pcgObject.PositionX.getter = () => this.NodePosX) pcgObject.PositionY && (pcgObject.PositionY.getter = () => this.NodePosY) pcgObject.getSubobjects().forEach( /** @param {ObjectEntity} obj */ obj => { if (obj.Node !== undefined) { const nodeRef = obj.Node.getter() if ( nodeRef.type === this.PCGNode.type && nodeRef.path === `${this.Name}.${this.PCGNode.path}` ) { obj.Node.getter = () => new ObjectReferenceEntity( this.PCGNode.type, `${this.Name}.${this.PCGNode.path}`, nodeRef.full, ) } } } ) } let inputIndex = 0 let outputIndex = 0 this.getCustomproperties().forEach((pinEntity, i) => { pinEntity.objectEntity = this pinEntity.pinIndex = pinEntity.isInput() ? inputIndex++ : pinEntity.isOutput() ? outputIndex++ : i }) this.mirrorNameInExportPaths() } /** @returns {P} */ static createGrammar() { return P.seq( P.reg(/Begin +Object/), P.seq( P.whitespace, P.alt( this.createSubObjectGrammar(), this.customPropertyGrammar, Grammar.createAttributeGrammar(this, P.reg(Grammar.Regex.MultipleWordsSymbols)), Grammar.createAttributeGrammar( this, Grammar.attributeNameQuoted, undefined, (values, attributeKey, attributeValue) => { Utility.objectSet(values, [...attributeKey, "quoted"], true) }, ), this.inlinedArrayEntryGrammar, ) ) .map(([_0, entry]) => entry) .many(), P.reg(/\s+End +Object/), ) .map(([_0, attributes, _2]) => { const values = {} attributes.forEach(attributeSetter => attributeSetter(values)) return new this(values) }) .label("ObjectEntity") } static createSubObjectGrammar() { return P.lazy(() => this.grammar) .map(object => values => { object.trailing = false values[Configuration.subObjectAttributeNameFromEntity(object)] = object } ) } /** * @protected * Mirror then name part of the objects contained in this one in ExportPath */ mirrorNameInExportPaths(originalName = this.Name?.toString()) { if (!originalName) { return } const values = [this] for (let i = 0; i < values.length; ++i) { const value = values[i] if (value instanceof ObjectEntity) { values.push(...Object.values(value)) if (!value.ExportPath?.valueOf().path.includes(originalName)) { continue } } else { continue } const mirroredEntity = /** @type {typeof ObjectEntity} */(value.constructor).attributes.ExportPath let originalExportPath = value.ExportPath value.ExportPath = new mirroredEntity( () => { const exportPath = originalExportPath.valueOf() return new (mirroredEntity.type)( exportPath.type, exportPath.path.replace(originalName, this.Name?.toString() ?? ""), exportPath.full ) } ) } } /** @type {String} */ #class getClass() { if (!this.#class) { this.#class = (this.Class?.path ? this.Class.path : this.Class?.type) ?? this.ExportPath?.valueOf()?.type ?? "" if (this.#class && !this.#class.startsWith("/")) { // Old path names did not start with /Script or /Engine, check tests/resources/LegacyNodes.js let path = Object.values(Configuration.paths).find(path => path.endsWith("." + this.#class)) if (path) { this.#class = path } } } return this.#class } getType() { const path = this.MacroGraphReference?.MacroGraph?.path if (path) { return path } if (this.MaterialExpression) { return this.MaterialExpression.type } let subobject = this.getSounCueSubobject() if (subobject) { return subobject.getClass() } return this.getClass() } getObjectName(dropCounter = false) { if (dropCounter) { return this.getNameAndCounter()[0] } return this.Name.toString() } /** @returns {[String, Number]} */ getNameAndCounter() { const result = this.getObjectName().match(ObjectEntity.#nameRegex) let name = "" let counter = null return result ? [result[1] ?? "", parseInt(result[2] ?? "0")] : ["", 0] } getCounter() { return this.getNameAndCounter()[1] } getNodeWidth() { return this.NodeWidth ?? this.isComment() ? Configuration.defaultCommentWidth : undefined } /** @param {Number} value */ setNodeWidth(value) { if (!this.NodeWidth) { this.NodeWidth = new IntegerEntity() } this.NodeWidth.value = value } getNodeHeight() { return this.NodeHeight ?? this.isComment() ? Configuration.defaultCommentHeight : undefined } /** @param {Number} value */ setNodeHeight(value) { if (!this.NodeHeight) { this.NodeHeight = new IntegerEntity() } this.NodeHeight.value = value } getNodePosX() { return this.NodePosX?.value ?? 0 } /** @param {Number} value */ setNodePosX(value) { if (!this.NodePosX) { this.NodePosX = new IntegerEntity() } this.NodePosX.value = Math.round(value) } getNodePosY() { return this.NodePosY?.value ?? 0 } /** @param {Number} value */ setNodePosY(value) { if (!this.NodePosY) { this.NodePosY = new IntegerEntity() } this.NodePosY.value = Math.round(value) } getCustomproperties(canCreate = false) { return this.CustomProperties.values } /** @returns {PinEntity[]} */ getPinEntities() { return this.getCustomproperties().filter(v => v.constructor === PinEntity) } /** @returns {ObjectEntity[]} */ getSubobjects() { return Object.keys(this) .filter(k => k.startsWith(Configuration.subObjectAttributeNamePrefix)) .flatMap(k => [this[k], .../** @type {ObjectEntity} */(this[k]).getSubobjects()]) } switchTarget() { const switchMatch = this.getClass().match(Configuration.switchTargetPattern) if (switchMatch) { return switchMatch[1] } } isEvent() { switch (this.getClass()) { case Configuration.paths.actorBoundEvent: case Configuration.paths.componentBoundEvent: case Configuration.paths.customEvent: case Configuration.paths.event: case Configuration.paths.inputAxisKeyEvent: case Configuration.paths.inputVectorAxisEvent: return true } return false } isComment() { switch (this.getClass()) { case Configuration.paths.comment: case Configuration.paths.materialGraphNodeComment: return true } return false } isMaterial() { const classValue = this.getClass() return classValue.startsWith("/Script/Engine.MaterialExpression") || classValue.startsWith("/Script/InterchangeImport.MaterialExpression") || classValue.startsWith("/Script/UnrealEd.MaterialGraph") } /** @return {ObjectEntity} */ getMaterialSubobject() { const expression = this.MaterialExpression ?? this.MaterialExpressionComment return expression ? this[Configuration.subObjectAttributeNameFromReference(expression, true)] : null } isPcg() { return this.getClass() == Configuration.paths.pcgEditorGraphNode || this.getPcgSubobject() != null } isNiagara() { return this.Class && (this.Class.type ? this.Class.type : this.Class.path)?.startsWith("/Script/NiagaraEditor.") } isSoundCue() { return this.getClass() == Configuration.paths.soundCueGraphNode } getBlueprintType() { if (this.isMaterial()) { return "MATERIAL" } if (this.isNiagara()) { return "NIAGARA" } if (this.isPcg()) { return "PCG Graph" } if (this.isSoundCue()) { return "SOUND CUE" } return "BLUEPRINT" } /** @return {ObjectEntity} */ getPcgSubobject() { const node = this.PCGNode return node ? this[Configuration.subObjectAttributeNameFromReference(node, true)] : null } /** @return {ObjectEntity} */ getSounCueSubobject() { const node = this.SoundNode return node ? this[Configuration.subObjectAttributeNameFromReference(node, true)] : null } /** @return {ObjectEntity} */ getSettingsObject() { const settings = this.SettingsInterface return settings ? this[Configuration.subObjectAttributeNameFromReference(settings, true)] : null } /** @return {ObjectEntity} */ getSubgraphObject() { const name = this.SubgraphInstance return name ? this[Configuration.subObjectAttributeNameFromName(name)] : null } isDevelopmentOnly() { const nodeClass = this.getClass() return this.EnabledState?.toString() === "DevelopmentOnly" || nodeClass.includes("Debug", Math.max(0, nodeClass.lastIndexOf("."))) } getHIDAttribute() { return this.InputKey ?? this.AxisKey ?? this.InputAxisKey } getDelegatePin() { return this.getCustomproperties().find(pin => pin.PinType.PinCategory.toString() === "delegate") } nodeColor() { return nodeColor(this) } nodeIcon() { return nodeIcon(this) } additionalPinInserter() { return nodeVariadic(this) } /** @param {String} key */ showProperty(key) { switch (key) { case "Class": case "Name": case "Archetype": case "ExportPath": case "CustomProperties": // Serielized separately, check doWrite() return false } return super.showProperty(key) } /** @param {typeof ObjectEntity} Self */ doSerialize( insideString = false, indentation = "", Self = /** @type {typeof ObjectEntity} */(this.constructor), printKey = Self.printKey, keySeparator = Self.keySeparator, attributeSeparator = Self.attributeSeparator, wrap = Self.wrap, ) { const isSelfOverriden = Self !== this.constructor const deeperIndentation = indentation + Configuration.indentation const initial_trailing = this.trailing this.trailing = true const content = super.doSerialize(insideString, deeperIndentation, Self, printKey, keySeparator, attributeSeparator, wrap) this.trailing = initial_trailing let result = indentation + "Begin Object" + ((this.Class?.type || this.Class?.path) // && Self.attributes.Class.ignored !== true // && this.Class.ignored !== true ? ` Class${keySeparator}${this.Class.serialize(insideString)}` : "" ) + (this.Name // && Self.attributes.Name.ignored !== true // && this.Name.ignored !== true ? ` Name${keySeparator}${this.Name.serialize(insideString)}` : "" ) + (this.Archetype // && Self.attributes.Archetype.ignored !== true // && this.Archetype.ignored !== true ? ` Archetype${keySeparator}${this.Archetype.serialize(insideString)}` : "" ) + ((this.ExportPath?.valueOf()?.type || this.ExportPath?.valueOf()?.path) // && Self.attributes.ExportPath.valueOf().ignored !== true // && this.ExportPath.valueOf().ignored !== true ? ` ExportPath${keySeparator}${this.ExportPath.serialize(insideString)}` : "" ) + attributeSeparator + content + (Self.attributes.CustomProperties.ignored !== true && this.CustomProperties.ignored !== true ? this.getCustomproperties() .map(pin => deeperIndentation + printKey("CustomProperties ") + pin.serialize(insideString) + attributeSeparator ) .join("") : "" ) + indentation + "End Object" + (isSelfOverriden && Self.trailing || this.trailing ? attributeSeparator : "") return result } }