Skip to content

Instantly share code, notes, and snippets.

@lowercasebtw
Last active March 31, 2025 15:59
Show Gist options
  • Select an option

  • Save lowercasebtw/40ee36c731672f44c5aa61fd75749132 to your computer and use it in GitHub Desktop.

Select an option

Save lowercasebtw/40ee36c731672f44c5aa61fd75749132 to your computer and use it in GitHub Desktop.
import { cp } from "node:fs/promises";
import process from "node:process";
type PathLike = string;
class Files {
static listdir(path: PathLike) {
return [...Deno.readDirSync(path)].map(c => c.name);
}
static async makedirs(path: PathLike) {
await Deno.mkdir(path, { recursive: true });
}
static async rmdir(path: PathLike) {
await Deno.remove(path, { recursive: true });
}
static async remove(path: PathLike) {
await Deno.remove(path);
}
static async copyTree(src: PathLike, dst: PathLike) {
if (!Paths.isdir(src)) {
throw new Error("Path is not a directory, couldn't copy.");
}
try {
await cp(src, dst, { recursive: true });
} catch (_) {
throw new Error("Failed to copy directory.");
}
}
static async rmTree(path: PathLike) {
await Deno.remove(path, { recursive: true });
}
static async move(src: PathLike, dst: PathLike) {
await Deno.rename(src, Paths.join(dst, Paths.basename(src)));
}
}
class Paths {
public static readonly FILE_SEPERATOR = process.platform != 'win32' ? '/' : '\\';
public static readonly EXT_SEPERATOR = '.';
static join(...parts: PathLike[]): PathLike {
return parts.join(this.FILE_SEPERATOR);
}
static exists(path: PathLike) {
try {
Deno.lstatSync(path);
return true;
} catch (_) {
return false;
}
}
static isdir(path: PathLike) {
try {
const fileInfo = Deno.statSync(path);
return fileInfo.isDirectory;
} catch (_) {
return false;
}
}
static splitext(path: PathLike) {
const sepIndex = path.lastIndexOf(this.FILE_SEPERATOR);
const dotIndex = path.lastIndexOf(this.EXT_SEPERATOR);
if (dotIndex > sepIndex) {
let filenameIndex = sepIndex + 1;
while (filenameIndex < dotIndex) {
if (path.charAt(filenameIndex) !== this.EXT_SEPERATOR) {
return [path.slice(0, dotIndex), path.slice(dotIndex)];
}
filenameIndex += 1;
}
}
return [path, ''];
}
static basename(path: PathLike) {
return path.substring(path.lastIndexOf(this.FILE_SEPERATOR) + 1);
}
static dirname(path: PathLike) {
const index = path.lastIndexOf(this.FILE_SEPERATOR) + 1;
const head = path.slice(0, index);
if (head && head !== this.FILE_SEPERATOR.repeat(head.length)) {
return head.replace(new RegExp(this.FILE_SEPERATOR + '$'), '');
} else {
return head;
}
}
}
function parseProperties(input: string): Record<string, string> {
const dict: Record<string, string> = {};
for (const line of input.split('\n')) {
const parts = line.split('=');
const key = parts[0].trim();
const value = parts.slice(1, parts.length).join('=').trim();
dict[key] = value;
}
return dict;
}
// CODE
const CONVERSION_SUFFIX = "_converted";
const TEXTURES_CIT_SUFFIX = "cit_textures";
const MINECRAFT_PATH = Paths.join("assets", "minecraft");
const ATLASES_PATH = Paths.join(MINECRAFT_PATH, "atlases");
const TEXTURES_PATH = Paths.join(MINECRAFT_PATH, "textures");
const OPTIFINE_PATH = Paths.join(MINECRAFT_PATH, "optifine");
const ITEMS_PATH = Paths.join(MINECRAFT_PATH, "items");
const MODELS_PATH = Paths.join(MINECRAFT_PATH, "models");
const ITEM_MODELS_PATH = Paths.join(MODELS_PATH, "item");
const CIT_PATH = Paths.join(OPTIFINE_PATH, "cit");
class NamespacedKey {
constructor(public namespace: string, public path: string) { }
static fromString(input: string): NamespacedKey {
if (input.length == 0) {
throw new Error("Invalid namespaced identifier");
}
const parts = input.split(':');
if (parts.length > 2) {
throw new Error("Invalid namespaced identifier");
}
const is_single = parts.length == 1;
return new NamespacedKey(
is_single ? "minecraft" : parts[0],
is_single ? parts[0] : parts[1]
);
}
equals(other: object) {
if (other instanceof NamespacedKey) {
return other.namespace == this.namespace && other.path == this.path;
} else {
return false;
}
}
toString() {
return `${this.namespace}:${this.path}`;
}
}
class Optional<T> {
private has_value: boolean;
constructor(public value: T) {
this.has_value = value != null && value != undefined;
}
isEmpty() {
return !this.has_value;
}
static ofNullable<T>(value: T) {
return new Optional<T>(value);
}
static of<T>(value: T) {
if (value == null || value == undefined) {
throw new Error("Value is null");
}
return new Optional<T>(value);
}
static empty() {
return Optional.ofNullable<any>(null);
}
}
type Range = number | {
min?: number;
max?: number;
};
function parseRange(input: string): Range {
if (input.includes('-')) {
const parts = input.split('-');
return {
min: parseInt(parts[0]),
max: parseInt(parts[1])
};
} else {
return parseInt(input);
}
}
function parseRanges(inputs: string[]): Range[] {
return [...inputs].map(parseRange);
}
enum Hand {
ANY,
MAIN,
OFF // ?
}
function handFromString(input: string) {
switch (input) {
case "any": return Hand.ANY;
case "main": return Hand.MAIN;
case "off": return Hand.OFF;
default: {
console.log("Recieved unknown hand type, defaulting to ANY");
return Hand.ANY;
}
}
}
class OptiFineCITItem {
public name: string;
public texture: string;
public items: NamespacedKey[];
public enchantments: NamespacedKey[];
public enchantment_levels: Range[];
public modelPath: string | null;
public damage: Range;
public stackSize: Range[];
public hand: Hand;
constructor(name: string, texture: string, items: NamespacedKey[]) {
this.name = name;
this.texture = texture;
this.items = items;
this.enchantments = [];
this.enchantment_levels = [];
this.modelPath = null;
this.damage = -1;
this.stackSize = [];
this.hand = Hand.ANY;
}
static parse(path: PathLike, name: string, input: string) {
const properties = parseProperties(input);
if (!("items" in properties)) {
console.error("Failed to parse OptiFineCITItem, missing \"items\" key");
return Optional.empty();
}
if (!("type" in properties || properties.type != "item")) {
console.error("Failed to parse OptiFineCITItem, not a item");
return Optional.empty();
}
const citItem = new OptiFineCITItem(name, Paths.join(path, `${name}.png`), properties.items.split(' ').map(NamespacedKey.fromString));
if ("texture" in properties) {
citItem.withTexture(properties.texture);
}
if ("enchantmentIDs" in properties) {
citItem.withEnchantments(properties.enchantmentIDs);
}
if ("enchantmentLevels" in properties) {
const levels: Range[] = [];
const ranges = parseRanges(properties.enchantmentLevels.split(' '));
if (typeof ranges == "number") {
levels.push(ranges);
} else {
levels.push(...ranges);
}
citItem.withEnchantmentLevels(levels);
}
if ("model" in properties) {
citItem.withModel(properties.model);
}
if ("damage" in properties) {
citItem.withDamage(parseRange(properties.damage));
}
if ("stackSize" in properties) {
citItem.withStackSizes(parseRanges(properties.stackSize.split(' ')));
}
if ("hand" in properties) {
citItem.withHand(handFromString(properties.hand));
}
return Optional.of(citItem);
}
// TODO: Support real syntax as this is probably totally wrong for anything else LOL
withTexture(texture: string) {
this.texture = texture;
return this;
}
withEnchantments(enchantments: string) {
this.enchantments = enchantments.split(' ').map(NamespacedKey.fromString);
return this;
}
withEnchantmentLevels(levels: Range[]) {
this.enchantment_levels = levels;
return this
}
withModel(modelPath: string) {
this.modelPath = modelPath;
return this;
}
withDamage(damage: Range) {
this.damage = damage;
return this;
}
withStackSizes(stackSizes: Range[]) {
this.stackSize = stackSizes;
return this;
}
withHand(hand: Hand) {
this.hand = hand;
return this;
}
}
function mapPaths(root: PathLike, paths: string[]) {
return paths.map(path => Paths.join(root, path));
}
async function ensurePath(path: PathLike) {
if (!Paths.exists(path)) {
await Files.makedirs(path);
}
return path;
}
interface VanillaItemDefenition {
type: string;
model?: string;
predicate?: string;
value?: any[];
on_false?: VanillaItemDefenition;
on_true?: VanillaItemDefenition;
}
function generateConditionEnchantmentsBranch(itemId: NamespacedKey, entry: DuplicateRecordEntry, on_false: VanillaItemDefenition) {
return {
type: "minecraft:condition",
property: "minecraft:component",
predicate: itemId.equals(new NamespacedKey("minecraft", "enchanted_book")) ? "stored_enchantments" : "enchantments",
value: entry.value,
on_true: {
type: "minecraft:model",
model: `minecraft:item/cit/${entry.name}`
},
on_false
} as VanillaItemDefenition;
}
function generateConditionEnchantments(key: NamespacedKey, entries: DuplicateRecordEntry[]) {
let dict: VanillaItemDefenition = {
type: "minecraft:model",
model: `${key.namespace}:item/${key.path}`
};
for (const entry of entries) {
dict = generateConditionEnchantmentsBranch(key, entry, dict);
}
return dict;
}
async function generateAtlas(dst: PathLike) {
const NEW_ATLASES_PATH = await ensurePath(Paths.join(dst, ATLASES_PATH));
// TODO: Append to existing blocks.json if it exists
await Deno.writeTextFile(
Paths.join(NEW_ATLASES_PATH, "blocks.json"),
JSON.stringify({
sources: [
{
type: "directory",
source: TEXTURES_CIT_SUFFIX,
prefix: `textures/${TEXTURES_CIT_SUFFIX}/`
}
]
}, null, 4));
}
async function filterItems(citPath: PathLike) {
let failed = false;
const citItems: OptiFineCITItem[] = [];
for (const propertiesFile of mapPaths(citPath, Files.listdir(citPath).filter(path => path.endsWith(".properties")))) {
// with open(properties_file) as file:
console.log(` Converting ${propertiesFile}`);
const file = await Deno.readTextFile(propertiesFile);
const citOptional = OptiFineCITItem.parse(Paths.dirname(propertiesFile), Paths.splitext(Paths.basename(propertiesFile))[0], file);
if (citOptional.isEmpty()) {
failed = true;
break;
}
citItems.push(citOptional.value);
await Files.remove(propertiesFile);
}
return {
result: citItems,
failed
};
}
interface EnchantmentEntry {
enchantments: string | string[];
levels?: number | {
min?: number;
max?: number;
}
}
interface DuplicateRecordEntry {
name: string;
texture: PathLike;
value: EnchantmentEntry[];
}
type DuplicateRecord = Record<string, Array<DuplicateRecordEntry>>;
function mapDuplicateItems(items: Array<OptiFineCITItem>) {
const duplicates: DuplicateRecord = {};
for (const citItem of items) {
const entry: DuplicateRecordEntry = {
name: citItem.name,
texture: citItem.texture,
value: []
};
if (citItem.enchantments.length > 0) {
// TODO: Do this smarter
for (const enchantment of citItem.enchantments) {
if (citItem.enchantment_levels.length > 0) {
for (const level of citItem.enchantment_levels) {
entry.value.push({
enchantments: enchantment.toString(),
levels: level
});
}
} else {
entry.value.push({
enchantments: enchantment.toString()
});
}
}
}
// TODO: Support multiple items
if (citItem.items[0].toString() in duplicates) {
duplicates[citItem.items[0].toString()].push(entry);
} else {
duplicates[citItem.items[0].toString()] = [entry];
}
}
return duplicates;
}
async function cleanup(dst: PathLike, citPath: PathLike) {
console.log("Cleaning up...");
if (Files.listdir(citPath).length == 0) {
await Files.rmdir(citPath);
}
const NEW_OPTIFINE_PATH = Paths.join(dst, OPTIFINE_PATH);
if (Files.listdir(NEW_OPTIFINE_PATH).length == 0) {
await Files.rmdir(NEW_OPTIFINE_PATH);
}
}
async function vanillaify(src: PathLike, dst: PathLike) {
const THIS_CIT_PATH = Paths.join(src, CIT_PATH);
if (!Paths.exists(THIS_CIT_PATH) || !Paths.isdir(THIS_CIT_PATH)) {
console.log(`${src} did not contain any OptiFine CIT, ignorning...`);
return;
}
console.log(`Converting ${src} to vanilla CIT pack`);
await Files.copyTree(src, dst);
const NEW_CIT_PATH = Paths.join(dst, CIT_PATH);
const NEW_ITEMS_PATH = await ensurePath(Paths.join(dst, ITEMS_PATH));
const NEW_TEXTURES_PATH = await ensurePath(Paths.join(dst, TEXTURES_PATH));
const PACK_TEXTURES_PATH = Paths.join(NEW_TEXTURES_PATH, TEXTURES_CIT_SUFFIX);
await Files.makedirs(PACK_TEXTURES_PATH);
await generateAtlas(dst);
const { result: citItems, failed } = await filterItems(NEW_CIT_PATH);
if (failed) {
await Files.rmTree(dst);
console.error("Failed!");
return;
}
// Group into singletons
const CIT_ITEM_MODELS_PATH = await ensurePath(Paths.join(dst, ITEM_MODELS_PATH, "cit"));
const duplicates = mapDuplicateItems(citItems);
for await (const key of Object.keys(duplicates)) {
// Item Models
const entry = duplicates[key];
for await (const item of entry) {
const itemName = item.name; // TODO: Fix, use item.texture
// TODO: Possibly get original model and swap layer0 texture or whatever
await Deno.writeTextFile(
Paths.join(CIT_ITEM_MODELS_PATH, `${itemName}.json`),
JSON.stringify(
{
parent: "minecraft:item/generated", // TODO: Get real parent for item type
textures: {
layer0: `minecraft:textures/${TEXTURES_CIT_SUFFIX}/${itemName}`
}
}, null, 4
),
{ create: true }
);
// Move Texture
if (Paths.exists(item.texture)) {
await Files.move(item.texture, PACK_TEXTURES_PATH);
}
}
// Item Defenitions
const namespacedKey = NamespacedKey.fromString(key);
const itemPath = Paths.join(NEW_ITEMS_PATH, `${namespacedKey.path}.json`);
// exists = path.exists(item_path)
// TODO
// if exists:
// original = json.loads(file.read())
// if "model" in original:
// defenition.model['on_false'] = original['model']
// file.seek(0)
await Deno.writeTextFile(
itemPath,
JSON.stringify({ model: generateConditionEnchantments(namespacedKey, entry) }, null, 4)
);
}
// Cleanup
await cleanup(dst, NEW_CIT_PATH);
}
async function main() {
const folders = mapPaths('.', Files.listdir('.').filter(key => Paths.isdir(key) && !key.endsWith(CONVERSION_SUFFIX)))
const promises = [];
for await (const folder of folders) {
promises.push(async () => {
const CONVERTED_PATH_NAME = `${folder}${CONVERSION_SUFFIX}`
if (Paths.exists(CONVERTED_PATH_NAME)) {
await Files.rmTree(CONVERTED_PATH_NAME);
console.log(`Found pre-existing conversion for ${folder}, removing...`);
}
const files = Files.listdir(folder);
if (!files.includes("pack.mcmeta")) {
console.log(`Skipping ${folder} as it is not a minecraft resourcepack`);
return;
}
await vanillaify(folder, CONVERTED_PATH_NAME)
});
}
(await Promise.all(promises)).forEach((promise) => promise());
console.log("Done!");
}
await main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment