Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
[MCEdit 1.x Filter] Fix duplicate UUIDs for https://bugs.mojang.com/browse/MC-95649
# Created by Marcono1234
# Feel free to modify this code and use it for your own, you don't have to mention me as creator (only for this filter)
from pymclevel import TAG_Compound, TAG_List, TAG_Int_Array, TAG_Byte_Array, TAG_String, TAG_Long, TAG_Int, TAG_Short, TAG_Byte, TAG_Double, TAG_Float
from albow import alert
from copy import deepcopy
import uuid
import time
displayName = "Fix duplicate UUIDs"
inputValues = [
"Action: "
]
inputDropdownValues = {}
inputDropdownValues[inputValues[0]] = (
"Assign new UUID",
"Delete duplicate entity",
"Delete if same NBT otherwise assign new UUID"
)
inputs = (
("Assigning a new UUID does not test if this UUID is already used by a player or the ender dragon. In these cases the game normally deletes the entity.", "label"),
("This filter affects all entities and ignores the selection. Therefor it takes less time if you select only a single block since MCEdit automatically creates a backup of every selected chunk.", "label"),
(inputValues[0], inputDropdownValues[inputValues[0]]),
)
# Kind of enum (http://stackoverflow.com/a/1695250)
def enum(**enums):
return type('Enum', (), enums)
ActionEnum = enum(
ASSIGN_NEW_UUID=1,
DELETE_DUPLICATE_ENTITY=2,
DELETE_IF_SAME_NBT=3,
)
uuidLeastKey = "UUIDLeast"
uuidMostKey = "UUIDMost"
entityIdKey = "id"
# Remove all tags that could have changed when the duplicate entity walked away
# Tags that are not changed or only changed on interaction with the player will remain
tagsToRemove = [
"Air",
"FallDistance",
"Dimension",
"Fire",
"Motion",
"OnGround",
"PortalCooldown",
"Rotation",
"Pos",
uuidLeastKey,
uuidMostKey,
]
maxUuidGenerationTries = 100
def perform(level, box, options):
chunks = level.getChunks()
usedUuidsList = []
# This map stores entities based on their UUID before a new UUID is assigned
originalUuidMap = {}
assignNewUuidList = []
action = None
if options[inputValues[0]] == inputDropdownValues[inputValues[0]][0]:
action = ActionEnum.ASSIGN_NEW_UUID
elif options[inputValues[0]] == inputDropdownValues[inputValues[0]][1]:
action = ActionEnum.DELETE_DUPLICATE_ENTITY
elif options[inputValues[0]] == inputDropdownValues[inputValues[0]][2]:
action = ActionEnum.DELETE_IF_SAME_NBT
totalChunksCount = level.chunkCount
chunkIndex = 0
printFilterMessage("Starting reading chunks. " + str(totalChunksCount) + " chunks exist")
chunkProgressUpdater = ProgressUpdater(0.05, "Reading chunks: ")
startTime = time.time()
summaryMap = {}
SummaryEnum = enum(
DUPLICATE_ENTITIES = "Duplicate entities",
NEW_ASSIGNED_UUIDS = "New assigned UUIDs",
DELETED_ENTITIES = "Deleted entities",
MODIFIED_CHUNK_COUNT = "Modified chunks",
TIME_NEEDED = "Time needed",
ENTITY_ID_DISTRIBUTION = "Entity id distribution",
)
summaryMap[SummaryEnum.NEW_ASSIGNED_UUIDS] = 0
summaryMap[SummaryEnum.DELETED_ENTITIES] = 0
modifiedChunks = 0
duplicatedEntities = 0
totalEntities = 0
entityIdDistribution = {}
for chunk in chunks:
chunkIndex += 1
deletedEntitiesCount = 0
chunkModified = False
totalEntities += len(chunk.Entities)
# Calling len(chunk.Entities) to get the updated length after deleting items
for entityIndex in xrange(0, len(chunk.Entities)):
updatedIndex = entityIndex - deletedEntitiesCount
entity = chunk.Entities[updatedIndex]
if uuidLeastKey in entity and uuidMostKey in entity:
uuidString = str(getUuid(entity[uuidMostKey].value, entity[uuidLeastKey].value))
if uuidString in usedUuidsList:
duplicatedEntities += 1
if entityIdKey in entity:
entityId = entity[entityIdKey].value
if entityId in entityIdDistribution:
entityIdDistribution[entityId] += 1
else:
entityIdDistribution[entityId] = 1
chunkModified = True
actionTemp = action
if action == ActionEnum.DELETE_IF_SAME_NBT:
duplicateEntity = removeTagsFromCompound(entity, tagsToRemove)
storedEntitiesList = originalUuidMap[uuidString]
for storedEntity in storedEntitiesList:
storedEntity = removeTagsFromCompound(storedEntity, tagsToRemove)
if (areNbtObjectsEqual(storedEntity, duplicateEntity)):
actionTemp = ActionEnum.DELETE_DUPLICATE_ENTITY
break
originalUuidMap[uuidString].append(entity)
# If the action is still DELETE_IF_SAME_NBT the NBT data was not the same
# as the one of the already stored entities
if actionTemp == ActionEnum.DELETE_IF_SAME_NBT:
actionTemp = ActionEnum.ASSIGN_NEW_UUID
if actionTemp == ActionEnum.ASSIGN_NEW_UUID:
assignNewUuidList.append(entity)
elif actionTemp == ActionEnum.DELETE_DUPLICATE_ENTITY:
del chunk.Entities[updatedIndex]
deletedEntitiesCount += 1
summaryMap[SummaryEnum.DELETED_ENTITIES] += 1
else:
usedUuidsList.append(uuidString)
if action == ActionEnum.DELETE_IF_SAME_NBT:
originalUuidMap[uuidString] = [entity]
else:
printFilterMessage("Entity has incomplete UUID: UUIDLeast and / or UUIDMost tag is missing")
if chunkModified:
chunk.dirty = True
modifiedChunks += 1
chunkProgressUpdater.updateProgress(chunkIndex / float(totalChunksCount))
# Assign new UUIDs here because now the usedUuidsList contains all used UUIDs
for entity in assignNewUuidList:
newUuid = uuid.uuid4()
retries = 0
while str(newUuid) in usedUuidsList and retries < maxUuidGenerationTries - 1:
newUuid = uuid.uuid4()
retries += 1
if retries == 100:
printFilterMessage("Tried generating a UUID as replacement for " + uuidString + " " +
maxUuidGenerationTries + " times, always generated already used UUID. Skipping entity.")
else:
splittedUuid = getUuidMostAndUuidLeast(newUuid)
entity[uuidMostKey] = TAG_Long(splittedUuid[uuidMostKey])
entity[uuidLeastKey] = TAG_Long(splittedUuid[uuidLeastKey])
usedUuidsList.append(str(newUuid))
summaryMap[SummaryEnum.NEW_ASSIGNED_UUIDS] += 1
reversedEntityIdDistribution = {}
for entityId in entityIdDistribution:
value = entityIdDistribution[entityId]
if value in reversedEntityIdDistribution:
reversedEntityIdDistribution[value].append(entityId)
else:
reversedEntityIdDistribution[value] = [entityId]
summaryMap[SummaryEnum.ENTITY_ID_DISTRIBUTION] = "\n\t\t" + "\n\t\t".join([(str(value) + ": " + ", ".join(sorted(reversedEntityIdDistribution[value]))) for value in reversed(sorted(reversedEntityIdDistribution))])
summaryMap[SummaryEnum.MODIFIED_CHUNK_COUNT] = str(modifiedChunks) + " / " + str(totalChunksCount)
summaryMap[SummaryEnum.DUPLICATE_ENTITIES] = str(duplicatedEntities) + " / " + str(totalEntities)
endTime = time.time()
summaryMap[SummaryEnum.TIME_NEEDED] = time.strftime("%H:%M:%S", time.gmtime(endTime - startTime))
summaryMessage = "Summary:\n\t" + "\n\t".join([(key + ": " + str(summaryMap[key])) for key in sorted(summaryMap)])
if duplicatedEntities > 0:
printFilterMessage(summaryMessage)
alert(summaryMessage)
else:
printFilterMessage(summaryMessage)
raise Exception(summaryMessage)
def getUuid(mostSignificantBits, leastSignificantBits):
return uuid.UUID(int=(((mostSignificantBits & 0xFFFFFFFFFFFFFFFF) << 64) | (leastSignificantBits & 0xFFFFFFFFFFFFFFFF)))
def getUuidMostAndUuidLeast(uuidValue):
intValue = uuidValue.int
uuidMap = {}
uuidMap[uuidMostKey] = changeToSignedValueWithBitCount(intValue >> 64, 64)
uuidMap[uuidLeastKey] = changeToSignedValueWithBitCount(intValue, 64)
return uuidMap
def changeToSignedValueWithBitCount(value, bitCount):
fullBits = (2 ** bitCount) - 1
# Shift only by bitCount - 1 because we want to shift the bit at bitCount to
# position 1 instead of 0
if (value >> (bitCount - 1)) & 1 == 1:
return ~(value & fullBits) ^ fullBits
else:
return (value & fullBits)
def printFilterMessage(message):
print ("[" + time.strftime("%H:%M:%S", time.localtime()) + " " + str(time.timezone) + "][Filter][" + displayName + "] " + message)
class ProgressUpdater:
steps = None
# Explanation: currentValue = int(decimal_value * 10000)
currentValue = 0
def __init__(self, steps, prefix = ""):
self.steps = int(steps * 10000)
self.prefix = prefix
def updateProgress(self, value):
value = int(value * 10000)
currentValueTemp = self.currentValue
while currentValueTemp <= value:
currentValueTemp += self.steps
currentValueTemp -= self.steps
if currentValueTemp > self.currentValue:
percentValue = currentValueTemp / float(100)
padding = " " * (3 - len(str(int(currentValueTemp))))
percentValueString = str(percentValue)
# http://stackoverflow.com/a/11227878
if "." in percentValueString:
percentValueString = percentValueString.rstrip("0").rstrip(".")
printFilterMessage(self.prefix + "Progress " + padding + percentValueString + "%")
self.currentValue = currentValueTemp
def removeTagsFromCompound(nbtCompound, tags):
if (type(nbtCompound) is TAG_Compound):
nbtCompoundCopy = deepcopy(nbtCompound)
for tag in tags:
if tag in nbtCompoundCopy:
del nbtCompoundCopy[tag]
return nbtCompoundCopy
else:
return None
def areNbtObjectsEqual(nbtObject1, nbtObject2):
nbtType1 = type(nbtObject1)
if nbtType1 is type(nbtObject2):
if nbtType1 is TAG_Compound:
for key in nbtObject1:
if key in nbtObject2:
if not areNbtObjectsEqual(nbtObject1[key], nbtObject2[key]):
return False
else:
return False
return True
elif nbtType1 is TAG_List:
for item1 in nbtObject1.value:
foundItem = False
for item2 in nbtObject2.value:
if areNbtObjectsEqual(item1, item2):
foundItem = True
break
if not foundItem:
return False
return True
# TAG_Int_Array, TAG_Byte_Array, TAG_String, TAG_Long, TAG_Int, TAG_Short, TAG_Byte, TAG_Double, TAG_Float
else:
return nbtObject1.value == nbtObject2.value
else:
return False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.