from panda3d.core import * from direct.interval.IntervalGlobal import * from direct.showbase.PythonUtil import lineInfo, Functor from direct.directnotify import DirectNotifyGlobal from direct.distributed import DistributedObject from otp.level import Level from otp.level import LevelConstants from otp.level import Entity from otp.level import EditMgr from .SpecImports import * from .InGameEditorElements import * from toontown.cogdominium import CogdoEntityCreator import string class InGameEditorEntityBase(InGameEditorElement): def __init__(self): InGameEditorElement.__init__(self) def attribChanged(self, attrib, value): Entity.Entity.attribChanged(self, attrib, value) print('attribChange: %s %s, %s = %s' % (self.level.getEntityType(self.entId), self.entId, attrib, repr(value))) def getTypeName(self): return self.level.getEntityType(self.entId) def privGetNamePrefix(self): return '[%s-%s] ' % (self.getTypeName(), self.entId) def privGetEntityName(self): return self.level.levelSpec.getEntitySpec(self.entId)['name'] def getName(self): return '%s%s' % (self.privGetNamePrefix(), self.privGetEntityName()) def setNewName(self, newName): prefix = self.privGetNamePrefix() if newName[:len(prefix)] == prefix: newName = newName[len(prefix):] oldName = self.privGetEntityName() if oldName != newName: self.level.setAttribEdit(self.entId, 'name', newName) def setParentEntId(self, parentEntId): self.parentEntId = parentEntId self.level.buildEntityTree() def setName(self, name): self.name = name self.level.buildEntityTree() class InGameEditorEntity(Entity.Entity, InGameEditorEntityBase): def __init__(self, level, entId): Entity.Entity.__init__(self, level, entId) InGameEditorEntityBase.__init__(self) def id(self): return self.entId def destroy(self): Entity.Entity.destroy(self) class InGameEditorEditMgr(EditMgr.EditMgr, InGameEditorEntityBase): def __init__(self, level, entId): EditMgr.EditMgr.__init__(self, level, entId) InGameEditorEntityBase.__init__(self) def destroy(self): EditMgr.EditMgr.destroy(self) class AttribModifier(Entity.Entity, InGameEditorEntityBase): notify = DirectNotifyGlobal.directNotify.newCategory('AttribModifier') def __init__(self, level, entId): Entity.Entity.__init__(self, level, entId) InGameEditorEntityBase.__init__(self) def destroy(self): Entity.Entity.destroy(self) def setValue(self, value): if len(self.typeName) == 0: AttribModifier.notify.warning('no typeName set') return entTypeReg = self.level.entTypeReg if self.typeName not in entTypeReg.getAllTypeNames(): AttribModifier.notify.warning('invalid typeName: %s' % self.typeName) return typeDesc = entTypeReg.getTypeDesc(self.typeName) if len(self.attribName) == 0: AttribModifier.notify.warning('no attribName set') return if self.attribName not in typeDesc.getAttribNames(): AttribModifier.notify.warning('invalid attribName: %s' % self.attribName) return if len(value) == 0: AttribModifier.notify.warning('no value set') def setAttrib(entId, typeName = self.typeName, attribName = self.attribName, value = eval(value), recursive = self.recursive): if typeName == self.level.getEntityType(entId): self.level.setAttribEdit(entId, attribName, value) if recursive: entity = self.level.getEntity(entId) for child in entity.getChildren(): setAttrib(child.entId) setAttrib(self.parentEntId) def getInGameEditorEntityCreatorClass(level): entCreator = level.createEntityCreator() EntCreatorClass = entCreator.__class__ class InGameEditorEntityCreator(EntCreatorClass): def __init__(self, editor): EntCreatorClass.__init__(self, editor) entTypes = list(self.entType2Ctor.keys()) for type in entTypes: self.entType2Ctor[type] = InGameEditorEntity self.entType2Ctor['editMgr'] = InGameEditorEditMgr self.entType2Ctor['attribModifier'] = AttribModifier return InGameEditorEntityCreator class DistributedInGameEditor(DistributedObject.DistributedObject, Level.Level, InGameEditorElement): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedInGameEditor') def __init__(self, cr): DistributedObject.DistributedObject.__init__(self, cr) Level.Level.__init__(self) InGameEditorElement.__init__(self) self.editorInitialized = 0 self.specModified = 0 self.undoStack = [] self.redoStack = [] self.entCreateHandlerQ = [] self.entitiesWeCreated = [] self.nodePathId2EntId = {} def generate(self): self.notify.debug('generate') DistributedObject.DistributedObject.generate(self) base.inGameEditor = self def setEditorAvId(self, editorAvId): self.editorAvId = editorAvId def setEditUsername(self, editUsername): self.editUsername = editUsername def getEditUsername(self): return self.editUsername def setLevelDoId(self, levelDoId): self.levelDoId = levelDoId self.level = base.cr.doId2do[self.levelDoId] def getLevelDoId(self): return self.levelDoId def announceGenerate(self): self.notify.debug('announceGenerate') DistributedObject.DistributedObject.announceGenerate(self) if self.editorIsLocalToon(): from otp.level import EditorGlobals EditorGlobals.assertReadyToEdit() self.notify.debug('requesting an up-to-date copy of the level spec') self.sendUpdate('requestCurrentLevelSpec') def setSpecSenderDoId(self, doId): DistributedInGameEditor.notify.debug('setSpecSenderDoId: %s' % doId) blobSender = base.cr.doId2do[doId] def setSpecBlob(specBlob, blobSender = blobSender, self = self): blobSender.sendAck() from otp.level.LevelSpec import LevelSpec curSpec = eval(specBlob) self.gotCurrentSpec(curSpec) if blobSender.isComplete(): setSpecBlob(blobSender.getBlob()) else: evtName = self.uniqueName('specDone') blobSender.setDoneEvent(evtName) self.acceptOnce(evtName, setSpecBlob) def gotCurrentSpec(self, curSpec): self.entTypeReg = self.level.getEntityTypeReg() curSpec.setEntityTypeReg(self.entTypeReg) self.axis = loader.loadModel('models/misc/xyzAxis.bam') self.axis.setColorOff() self.axis.setColorScale(1, 1, 1, 1, 1) self.initializeLevel(self.doId, curSpec, curSpec.getScenario()) entCreator = self.level.createEntityCreator() self.entTypes = entCreator.getEntityTypes() self.selectedEntity = None base.startTk() from . import InGameEditor doneEvent = self.uniqueName('editorDone') saveAsEvent = self.uniqueName('saveSpec') requestSaveEvent = self.uniqueName('requestSpecSave') undoEvent = self.uniqueName('undoEvent') redoEvent = self.uniqueName('redoEvent') wireframeEvent = self.uniqueName('wireframeEvent') oobeEvent = self.uniqueName('oobeEvent') csEvent = self.uniqueName('csEvent') runEvent = self.uniqueName('runEvent') texEvent = self.uniqueName('texEvent') self.editor = InGameEditor.InGameEditor(level=self, doneEvent=doneEvent, requestSaveEvent=requestSaveEvent, saveAsEvent=saveAsEvent, undoEvent=undoEvent, redoEvent=redoEvent, wireframeEvent=wireframeEvent, oobeEvent=oobeEvent, csEvent=csEvent, runEvent=runEvent, texEvent=texEvent) self.acceptOnce(doneEvent, self.doneEditing) self.accept(saveAsEvent, self.saveSpec) self.accept(requestSaveEvent, self.requestSpecSave) self.accept(undoEvent, self.doUndo) self.accept(redoEvent, self.doRedo) self.accept(wireframeEvent, self.doWireframe) self.accept(oobeEvent, self.doOobe) self.accept(csEvent, self.doCs) self.accept(runEvent, self.doRun) self.accept(texEvent, self.doTex) self.accept(self.editor.getEventMsgName('Select'), self.handleEntitySelect) self.accept(self.editor.getEventMsgName('Flash'), self.handleEntitySelect) self.editorInitialized = 1 self.buildEntityTree() return def editorIsLocalToon(self): return self.editorAvId == base.localAvatar.doId def createEntityCreator(self): return getInGameEditorEntityCreatorClass(self.level)(self) def doneEditing(self): self.notify.debug('doneEditing') if self.specModified: if self.editor.askYesNo('Save the spec on the AI?'): self.requestSpecSave() self.sendUpdate('setFinished') def disable(self): self.notify.debug('disable') if self.editorInitialized and self.editorIsLocalToon(): self.axis.removeNode() del self.axis if hasattr(self, 'entTypeReg'): del self.entTypeReg self.editorInitialized = 0 Level.Level.destroyLevel(self) if hasattr(self, 'editor'): self.editor.quit() del self.editor DistributedObject.DistributedObject.disable(self) self.ignoreAll() def getEntInstance(self, entId): return self.level.getEntity(entId) def getEntInstanceNP(self, entId): entity = self.getEntInstance(entId) if entity is None: return if isinstance(entity, NodePath): return entity if hasattr(entity, 'getNodePath'): return entity.getNodePath() return def getEntInstanceNPCopy(self, entId): np = self.getEntInstanceNP(entId) if np is None: return np stashNodeGroups = [] searches = ('**/+ActorNode', '**/+Character') for search in searches: stashNodeGroups.append(np.findAllMatches(search)) for group in stashNodeGroups: if not group.isEmpty(): group.stash() par = np.getParent() copy = np.copyTo(par) for group in stashNodeGroups: if not group.isEmpty(): group.unstash() return copy def saveSpec(self, filename): return self.levelSpec.saveToDisk(filename) def setEntityParent(self, entity, parent): parent.addChild(entity) entity._parentEntity = parent def insertEntityIntoTree(self, entId): ent = self.getEntity(entId) if entId == LevelConstants.UberZoneEntId: self.setEntityParent(ent, self) return parentEnt = self.getEntity(ent.parentEntId) if parentEnt is not None: self.setEntityParent(ent, parentEnt) return self.setEntityParent(ent, self.uberZoneEntity) return def buildEntityTree(self): self.setChildren([]) entIds = list(self.entities.keys()) entIds.sort() for entId in entIds: ent = self.getEntity(entId) ent.setChildren([]) for entId in entIds: self.insertEntityIntoTree(entId) self.editor.refreshExplorer() def onEntityCreate(self, entId): DistributedInGameEditor.notify.debug('onEntityCreate %s' % entId) Level.Level.onEntityCreate(self, entId) entityNP = self.getEntInstanceNP(entId) if entityNP: self.nodePathId2EntId[entityNP.id()] = entId if not self.editorInitialized: return self.insertEntityIntoTree(entId) self.editor.refreshExplorer() if entId == self.entitiesWeCreated[0]: self.entitiesWeCreated = self.entitiesWeCreated[1:] self.editor.selectEntity(entId) def onEntityDestroy(self, entId): DistributedInGameEditor.notify.debug('onEntityDestroy %s' % entId) ent = self.getEntity(entId) if self.editorInitialized: entityNP = self.getEntInstanceNP(entId) if entityNP in self.nodePathId2EntId: del self.nodePathId2EntId[entityNP.id()] if ent is self.selectedEntity: self.editor.clearAttribEditPane() self.selectedEntity = None ent._parentEntity.removeChild(ent) del ent._parentEntity self.editor.refreshExplorer() Level.Level.onEntityDestroy(self, entId) return def handleEntitySelect(self, entity): self.selectedEntity = entity if hasattr(self, 'identifyIval'): self.identifyIval.finish() if entity is self: self.editor.clearAttribEditPane() else: entityNP = self.getEntInstanceNP(entity.entId) if entityNP is not None: dur = float(0.5) oColor = entityNP.getColorScale() flashIval = Sequence(Func(Functor(entityNP.setColorScale, 1, 0, 0, 1)), WaitInterval(dur / 3), Func(Functor(entityNP.setColorScale, 0, 1, 0, 1)), WaitInterval(dur / 3), Func(Functor(entityNP.setColorScale, 0, 0, 1, 1)), WaitInterval(dur / 3), Func(Functor(entityNP.setColorScale, oColor[0], oColor[1], oColor[2], oColor[3]))) boundIval = Sequence(Func(entityNP.showBounds), WaitInterval(dur * 0.5), Func(entityNP.hideBounds)) entCp = self.getEntInstanceNPCopy(entity.entId) entCp.setRenderModeWireframe() entCp.setTextureOff(1) wireIval = Sequence(Func(Functor(entCp.setColor, 1, 0, 0, 1, 1)), WaitInterval(dur / 3), Func(Functor(entCp.setColor, 0, 1, 0, 1, 1)), WaitInterval(dur / 3), Func(Functor(entCp.setColor, 0, 0, 1, 1, 1)), WaitInterval(dur / 3), Func(entCp.removeNode)) self.identifyIval = Parallel(flashIval, boundIval, wireIval) def putAxis(self = self, entityNP = entityNP): self.axis.reparentTo(entityNP) self.axis.setPos(0, 0, 0) self.axis.setHpr(0, 0, 0) def takeAxis(self = self): self.axis.reparentTo(hidden) self.identifyIval = Sequence(Func(putAxis), Parallel(self.identifyIval, WaitInterval(1000.5)), Func(takeAxis)) self.identifyIval.start() self.editor.updateAttribEditPane(entity.entId, self.levelSpec, self.entTypeReg) entType = self.getEntityType(entity.entId) menu = self.editor.menuBar.component('Entity-menu') index = menu.index('Remove Selected Entity') if entType in self.entTypeReg.getPermanentTypeNames(): menu.entryconfigure(index, state='disabled') else: menu.entryconfigure(index, state='normal') return def privSendAttribEdit(self, entId, attrib, value): self.specModified = 1 valueStr = repr(value) self.notify.debug("sending edit: %s: '%s' = %s" % (entId, attrib, valueStr)) self.sendUpdate('setEdit', [entId, attrib, valueStr, self.editUsername]) def privExecActionList(self, actions): for action in actions: if callable(action): action() else: entId, attrib, value = action self.privSendAttribEdit(entId, attrib, value) def setUndoableAttribEdit(self, old2new, new2old): self.redoStack = [] self.undoStack.append((new2old, old2new)) self.privExecActionList(old2new) def setAttribEdit(self, entId, attrib, value, canUndo = 1): oldValue = eval(repr(self.levelSpec.getEntitySpec(entId)[attrib])) new2old = [(entId, attrib, oldValue)] old2new = [(entId, attrib, value)] self.setUndoableAttribEdit(old2new, new2old) if not canUndo: self.undoStack = [] def doUndo(self): if len(self.undoStack) == 0: self.editor.showWarning('Nothing left to undo') return undo = self.undoStack.pop() self.redoStack.append(undo) new2old, old2new = undo self.privExecActionList(new2old) def doRedo(self): if len(self.redoStack) == 0: self.editor.showWarning('Nothing to redo') return redo = self.redoStack.pop() self.undoStack.append(redo) new2old, old2new = redo self.privExecActionList(old2new) def doWireframe(self): messenger.send('magicWord', ['~wire']) def doOobe(self): messenger.send('magicWord', ['~oobe']) def doCs(self): messenger.send('magicWord', ['~cs']) def doRun(self): messenger.send('magicWord', ['~run']) def doTex(self): messenger.send('magicWord', ['~tex']) def insertEntity(self, entType, parentEntId = None, callback = None): if parentEntId is None: try: parentEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid parent entity first.', 'error') return removeAction = (self.editMgrEntity.entId, 'removeEntity', {'entId': 'REPLACEME'}) new2old = [removeAction] def setNewEntityId(entId, self = self, action = removeAction, callback = callback): action[2]['entId'] = entId if callback: callback(entId) def setEntCreateHandler(self = self, handler = setNewEntityId): self.entCreateHandlerQ.append(handler) old2new = [setEntCreateHandler, (self.editMgrEntity.entId, 'requestNewEntity', {'entType': entType, 'parentEntId': parentEntId, 'username': self.editUsername})] self.setUndoableAttribEdit(old2new, new2old) return def setEntityCreatorUsername(self, entId, editUsername): Level.Level.setEntityCreatorUsername(self, entId, editUsername) if editUsername == self.getEditUsername(): print('entity %s about to be created; we requested it' % entId) callback = self.entCreateHandlerQ[0] del self.entCreateHandlerQ[:1] callback(entId) self.entitiesWeCreated.append(entId) def removeSelectedEntity(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity to be removed first.', 'error') return -1 if self.getEntity(selectedEntId).getNumChildren() > 0: self.editor.showWarning('Remove children first.') return -1 self.doRemoveEntity(selectedEntId) def removeSelectedEntityTree(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity to be removed first.', 'error') return -1 def removeEntity(entId): entity = self.getEntity(entId) for child in entity.getChildren(): removeEntity(child.entId) self.doRemoveEntity(entId) removeEntity(selectedEntId) def doRemoveEntity(self, entId): parentEntId = self.getEntity(entId)._parentEntity.entId entType = self.getEntityType(entId) if entType in self.entTypeReg.getPermanentTypeNames(): self.editor.showWarning("Cannot remove entities of type '%s'" % entType) return removeAction = (self.editMgrEntity.entId, 'removeEntity', {'entId': entId}) old2new = [removeAction] oldAttribs = [] spec = self.levelSpec.getEntitySpecCopy(entId) del spec['type'] for attrib, value in list(spec.items()): oldAttribs.append((attrib, value)) def setNewEntityId(entId, self = self, removeAction = removeAction, oldAttribs = oldAttribs): removeAction[2]['entId'] = entId for attrib, value in list(spec.items()): self.privSendAttribEdit(entId, attrib, value) def setEntCreateHandler(self = self, handler = setNewEntityId): self.entCreateHandlerQ.append(handler) new2old = [setEntCreateHandler, (self.editMgrEntity.entId, 'requestNewEntity', {'entType': self.getEntityType(entId), 'parentEntId': parentEntId, 'username': self.editUsername})] self.setUndoableAttribEdit(old2new, new2old) def makeCopyOfEntName(self, name): prefix = 'copy of ' suffix = ' (%s)' oldName = name if len(oldName) <= len(prefix): newName = prefix + oldName elif oldName[:len(prefix)] != prefix: newName = prefix + oldName else: hasSuffix = True copyNum = 2 if oldName[-1] != ')': hasSuffix = False if hasSuffix and oldName[-2] in string.digits: i = len(oldName) - 2 numString = '' while oldName[i] in string.digits: numString = oldName[i] + numString i -= 1 if oldName[i] != '(': hasSuffix = False else: i -= 1 if oldName[i] != ' ': hasSuffix = False else: print('numString: %s' % numString) copyNum = int(numString) + 1 if hasSuffix: newName = oldName[:i] + suffix % copyNum else: newName = oldName + suffix % copyNum return newName def duplicateSelectedEntity(self): try: selectedEntId = self.selectedEntity.entId parentEntId = self.selectedEntity._parentEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity to be removed first.', 'error') return if self.selectedEntity.getNumChildren() > 0: self.editor.showTodo('Cannot duplicate entity with children.') return removeAction = (self.editMgrEntity.entId, 'removeEntity', {'entId': selectedEntId}) new2old = [removeAction] copyAttribs = self.levelSpec.getEntitySpecCopy(selectedEntId) copyAttribs['comment'] = '' copyAttribs['name'] = self.makeCopyOfEntName(copyAttribs['name']) typeDesc = self.entTypeReg.getTypeDesc(copyAttribs['type']) attribDescs = typeDesc.getAttribDescDict() for attribName, attribDesc in list(attribDescs.items()): if attribDesc.getDatatype() == 'const': del copyAttribs[attribName] def setNewEntityId(entId, self = self, removeAction = removeAction, copyAttribs = copyAttribs): removeAction[2]['entId'] = entId for attribName, value in list(copyAttribs.items()): self.privSendAttribEdit(entId, attribName, value) def setEntCreateHandler(self = self, handler = setNewEntityId): self.entCreateHandlerQ.append(handler) old2new = [setEntCreateHandler, (self.editMgrEntity.entId, 'requestNewEntity', {'entType': self.getEntityType(selectedEntId), 'parentEntId': parentEntId, 'username': self.editUsername})] self.setUndoableAttribEdit(old2new, new2old) def specPrePickle(self, spec): for attribName, value in list(spec.items()): spec[attribName] = repr(value) return spec def specPostUnpickle(self, spec): for attribName, value in list(spec.items()): spec[attribName] = eval(value) return spec def handleImportEntities(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity first.', 'error') return import tkinter.filedialog filename = tkinter.filedialog.askopenfilename(parent=self.editor.parent, defaultextension='.egroup', filetypes=[('Entity Group', '.egroup'), ('All Files', '*')]) if len(filename) == 0: return try: import pickle f = open(filename, 'r') eTree = pickle.load(f) eGroup = pickle.load(f) for entId, spec in list(eGroup.items()): eGroup[entId] = self.specPostUnpickle(spec) except: self.editor.showWarning("Error importing entity group from '%s'." % filename, 'error') return oldEntId2new = {} def addEntities(treeEntry, parentEntId, eGroup = eGroup): for entId, children in list(treeEntry.items()): spec = eGroup[entId] entType = spec['type'] del spec['type'] del spec['parentEntId'] typeDesc = self.entTypeReg.getTypeDesc(entType) for attribName, attribDesc in list(typeDesc.getAttribDescDict().items()): if attribDesc.getDatatype() == 'const': if attribName in spec: del spec[attribName] def handleEntityInsertComplete(newEntId, oldEntId = entId, oldEntId2new = oldEntId2new, spec = spec, treeEntry = treeEntry, addEntities = addEntities): oldEntId2new[oldEntId] = newEntId def assignAttribs(entId = newEntId, oldEntId = oldEntId, spec = spec, treeEntry = treeEntry): for attribName in spec: self.setAttribEdit(entId, attribName, spec[attribName]) addEntities(treeEntry[oldEntId], newEntId) self.acceptOnce(self.getEntityCreateEvent(newEntId), assignAttribs) self.insertEntity(entType, parentEntId=parentEntId, callback=handleEntityInsertComplete) addEntities(eTree, selectedEntId) def handleExportEntity(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity first.', 'error') return import tkinter.filedialog filename = tkinter.filedialog.asksaveasfilename(parent=self.editor.parent, defaultextension='.egroup', filetypes=[('Entity Group', '.egroup'), ('All Files', '*')]) if len(filename) == 0: return eTree = {selectedEntId: {}} eGroup = {} eGroup[selectedEntId] = self.levelSpec.getEntitySpecCopy(selectedEntId) for entId, spec in list(eGroup.items()): eGroup[entId] = self.specPrePickle(spec) try: import pickle f = open(filename, 'w') pickle.dump(eTree, f) pickle.dump(eGroup, f) except: self.editor.showWarning("Error exporting entity group to '%s'." % filename, 'error') return def handleExportEntityTree(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity first.', 'error') return import tkinter.filedialog filename = tkinter.filedialog.asksaveasfilename(parent=self.editor.parent, defaultextension='.egroup', filetypes=[('Entity Group', '.egroup'), ('All Files', '*')]) if len(filename) == 0: return eTree = {} eGroup = {} def addEntity(entId, treeEntry): treeEntry[entId] = {} eGroup[entId] = self.levelSpec.getEntitySpecCopy(entId) entity = self.getEntity(entId) for child in entity.getChildren(): addEntity(child.entId, treeEntry[entId]) addEntity(selectedEntId, eTree) for entId, spec in list(eGroup.items()): eGroup[entId] = self.specPrePickle(spec) try: import pickle f = open(filename, 'w') pickle.dump(eTree, f) pickle.dump(eGroup, f) except: self.editor.showWarning("Error exporting entity group to '%s'." % filename, 'error') return def moveAvToSelected(self): try: selectedEntId = self.selectedEntity.entId except AttributeError: self.editor.showWarning('Please select a valid entity first.', 'error') return entNp = self.getEntInstanceNP(selectedEntId) if entNp is None: zoneEntId = self.levelSpec.getEntityZoneEntId(selectedEntId) entNp = self.getEntInstanceNP(zoneEntId) base.localAvatar.setPos(entNp, 0, 0, 0) base.localAvatar.setHpr(entNp, 0, 0, 0) zoneNum = self.getEntityZoneEntId(selectedEntId) self.level.enterZone(zoneNum) return def requestSpecSave(self): self.privSendAttribEdit(LevelConstants.EditMgrEntId, 'requestSave', None) self.specModified = 0 return def setAttribChange(self, entId, attrib, valueStr, username): if username == self.editUsername: print('we got our own edit back!') value = eval(valueStr) self.levelSpec.setAttribChange(entId, attrib, value, username) def getTypeName(self): return 'Level'