Spaces:
Sleeping
Sleeping
// TODO: Extract this and TurboWarp/scratch-vm's compression to a shared module | |
// We don't generate new IDs using numbers at this time because their enumeration | |
// order can affect script execution order as they always come first. | |
// https://tc39.es/ecma262/#sec-ordinaryownpropertykeys | |
const SOUP = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#%()*+,-./:;=?@[]^_`{|}~'; | |
const generateId = i => { | |
let str = ''; | |
while (i >= 0) { | |
str = SOUP[i % SOUP.length] + str; | |
i = Math.floor(i / SOUP.length) - 1; | |
} | |
return str; | |
}; | |
class Pool { | |
constructor() { | |
this.generatedIds = new Map(); | |
this.references = new Map(); | |
this.skippedIds = new Set(); | |
// IDs in Object.keys(vm.runtime.monitorBlocks._blocks) already have meaning, so make sure to skip those | |
// We don't bother listing many here because most would take more than ten million items to be used | |
this.skippedIds.add('of'); | |
} | |
skip (id) { | |
this.skippedIds.add(id); | |
} | |
addReference(id) { | |
const currentCount = this.references.get(id) || 0; | |
this.references.set(id, currentCount + 1); | |
} | |
generateNewIds() { | |
const entries = Array.from(this.references.entries()); | |
// The most used original IDs should get the shortest new IDs. | |
entries.sort((a, b) => b[1] - a[1]); | |
let i = 0; | |
for (const entry of entries) { | |
const oldId = entry[0]; | |
let newId = generateId(i); | |
while (this.skippedIds.has(newId)) { | |
i++; | |
newId = generateId(i); | |
} | |
this.generatedIds.set(oldId, newId); | |
i++; | |
} | |
} | |
getNewId(originalId) { | |
if (this.generatedIds.has(originalId)) { | |
return this.generatedIds.get(originalId); | |
} | |
return originalId; | |
} | |
} | |
const optimizeSb3Json = (projectData) => { | |
// Note: we modify projectData in-place | |
// Scan global attributes of the project so we can generate optimal IDs later | |
const blockPool = new Pool(); | |
for (const target of projectData.targets) { | |
for (const [blockId, block] of Object.entries(target.blocks)) { | |
blockPool.addReference(blockId); | |
if (Array.isArray(block)) { | |
continue; | |
} | |
if (block.parent) { | |
blockPool.addReference(block.parent); | |
} | |
if (block.next) { | |
blockPool.addReference(block.next); | |
} | |
for (const input of Object.values(block.inputs)) { | |
for (let i = 1; i < input.length; i++) { | |
const inputValue = input[i]; | |
if (typeof inputValue === 'string') { | |
blockPool.addReference(inputValue); | |
} | |
} | |
} | |
} | |
} | |
blockPool.generateNewIds(); | |
if (projectData.monitors) { | |
for (const monitor of projectData.monitors) { | |
// Remove redundant monitor values | |
monitor.value = Array.isArray(monitor.value) ? [] : 0; | |
} | |
} | |
// Use gathered data to optimize the project | |
for (const target of projectData.targets) { | |
const newBlocks = {}; | |
const newComments = {}; | |
for (const [blockId, block] of Object.entries(target.blocks)) { | |
newBlocks[blockPool.getNewId(blockId)] = block; | |
if (Array.isArray(block)) { | |
continue; | |
} | |
if (block.parent) { | |
block.parent = blockPool.getNewId(block.parent); | |
} | |
if (block.next) { | |
block.next = blockPool.getNewId(block.next); | |
} | |
for (const input of Object.values(block.inputs)) { | |
for (let i = 1; i < input.length; i++) { | |
const inputValue = input[i]; | |
if (typeof inputValue === 'string') { | |
input[i] = blockPool.getNewId(inputValue); | |
} | |
} | |
} | |
if (!block.shadow) { | |
delete block.shadow; | |
} | |
if (!block.topLevel) { | |
delete block.topLevel; | |
} | |
delete block.x; | |
delete block.y; | |
delete block.comment; | |
} | |
for (const [commentId, comment] of Object.entries(target.comments)) { | |
const text = comment.text; | |
const isSpecial = text.includes(' // _twconfig_') || text.includes(' // _gamepad_'); | |
if (isSpecial) { | |
newComments[commentId] = comment; | |
} | |
} | |
target.blocks = newBlocks; | |
target.comments = newComments; | |
} | |
// Remove unnecessary metadata | |
if (projectData.meta) { | |
delete projectData.meta.agent; | |
delete projectData.meta.vm; | |
} | |
return projectData; | |
}; | |
export default optimizeSb3Json; | |