First commit

This commit is contained in:
CPTProgrammer 2024-12-10 01:49:42 +08:00
commit 30a4c17d79
9 changed files with 425 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
data
mods
node_modules
recipes
recipes.zip

44
README.md Normal file
View File

@ -0,0 +1,44 @@
# Minecraft Recipe Tools
使用前请先运行,以安装必要库
```bash
node install
```
### 文件夹说明
- `./data` 导出的合成配方
- `./mods` 放置待导出合成配方的模组
- `./recipes` 生成的数据包
### 文件作用
- `extract_recipes.js`:把 `./mods` 下所有模组的合成配方导出至 `./data` 文件夹下
- `generate.js`:根据 `settings.json` 配置,找出 `./data` 下需要修改的配方,并于 `./recipes` 生成数据包
### 配置文件
- `enableSearch: boolean` 启用搜索
- `enableGenerate: boolean` 启用生成数据包
- `same: Array<ItemMapping>` 物品映射列表
```typescript
type ItemMapping = {
tag: string; // Minecraft 标签
replace: boolean; // 是否覆盖原标签(同 Minecraft Tag JSON 文件)
list: Array<string>; // 应被视为相同物品的物品 ID 列表
}
- `search: SearchArgument` 搜索参数
```typescript
type SearchArgument = {
tags: Array<string>; // 要搜索的标签(不搜索合成结果)
items: Array<string>; // 要搜索的物品ID不搜索合成结果
}
```

101
extract_recipes.js Normal file
View File

@ -0,0 +1,101 @@
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip"); // 用于解压 .jar 文件
// 创建文件夹(如果不存在)
function createDirIfNotExist(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/*// 写入 pack.mcmeta 文件
function writePackMcmeta() {
const packMcmetaPath = "./recipes/pack.mcmeta";
const packContent = {
pack: {
description: "通用了部分整合包模组的物品",
pack_format: 9,
},
};
if (!fs.existsSync(packMcmetaPath)) {
fs.writeFileSync(
packMcmetaPath,
JSON.stringify(packContent, null, 2),
"utf8"
);
console.log("创建并写入 pack.mcmeta 文件");
}
}*/
// 获取 .jar 文件列表
function getJarFiles(dirPath) {
const files = fs.readdirSync(dirPath);
return files.filter((file) => file.endsWith(".jar"));
}
// 解压 .jar 文件中的特定目录
function extractRecipesFromJar(jarPath, dir) {
const zip = new AdmZip(jarPath);
const recipesPath = `data/${dir}/recipes/`;
const entries = zip.getEntries();
// 查找并解压 recipes 文件夹中的内容
entries.forEach((entry) => {
if (/^data\/[^\/]+\/recipes\/.*$/.test(entry.entryName)) {
const targetFile = path.join(__dirname, entry.entryName);
const targetPath = path.dirname(targetFile)
if (entry.isDirectory){
createDirIfNotExist(targetFile);
} else {
createDirIfNotExist(targetPath);
fs.writeFileSync(targetFile, entry.getData());
}
// console.log(`解压文件到 ${targetFile}`);
}
});
}
// 处理 mods 目录下的所有 .jar 文件
function processMods() {
const modsDir = "./mods";
// const recipesDir = "./recipes";
if (fs.existsSync("./data")) {
fs.rmSync("./data", { recursive: true, force: true });
}
// createDirIfNotExist(recipesDir); // 确保 ./recipes 文件夹存在
// writePackMcmeta(); // 确保 ./recipes/pack.mcmeta 文件存在
const jarFiles = getJarFiles(modsDir);
jarFiles.forEach((jarFile) => {
const jarPath = path.join(modsDir, jarFile);
console.log(`正在读取 ${jarPath}`);
const zip = new AdmZip(jarPath);
const dataEntry = zip.getEntry("data/");
// console.log(zip.getEntries().map((v) => v.entryName).filter((v) => /data\/[a-zA-Z]+\/recipes\/.*/.test(v)));
if (!dataEntry) {
console.log(`未找到 data 目录: ${jarPath}`);
return;
}
// 获取 /data 文件夹内的所有子文件夹目录
const dirList = [];
const entries = zip.getEntries();
entries.forEach((entry) => {
const match = entry.entryName.match(/^data\/([^\/]+)/);
if (match && !dirList.includes(match[1])) {
dirList.push(match[1]);
}
});
// 遍历并解压每个目录下的 recipes 文件夹
dirList.forEach((dir) => {
extractRecipesFromJar(jarPath, dir);
});
});
}
processMods();

15
extract_recipes.txt Normal file
View File

@ -0,0 +1,15 @@
按照此伪代码编写一个Node.js脚本要求
1. 尽量不使用第三方库adm-zip 除外用来解析jar包
2. 使用双引号代替单引号
3. 使用引号
伪代码:
如果 ./recipes 不存在,则创建 ./recipes 文件夹
如果 ./recipes/pack.mcmeta 不存在,则创建并写入格式化的 { "pack": { "description": "通用了部分整合包模组的物品", "pack_format": 9 } }
遍历读取 ./mods 下的所有 *.jar {
const dirList = 读取 *.jar 内部的 /data 文件夹里的子文件夹目录;
遍历 dirList {
解压 *.jar 内部的 `/data/${dir}/recipes` 至 `./data/${dir}/recipes`
}
}

163
generate.js Normal file
View File

@ -0,0 +1,163 @@
const fs = require("fs");
const path = require("path");
const settings = JSON.parse(fs.readFileSync("settings.json"));
const recipesDir = "./recipes";
// 创建文件夹(如果不存在)
function createDirIfNotExist(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// 写入 pack.mcmeta 文件
function writePackMcmeta() {
const packMcmetaPath = "./recipes/pack.mcmeta";
const packContent = {
pack: {
description: "通用了部分整合包模组的物品",
pack_format: 9,
},
};
if (!fs.existsSync(packMcmetaPath)) {
fs.writeFileSync(
packMcmetaPath,
JSON.stringify(packContent, null, 2),
"utf8"
);
console.log("创建并写入 pack.mcmeta 文件");
}
}
// 递归遍历文件夹并查找所有的 .json 文件
function traverseDir(dirPath) {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 如果是文件夹,则递归调用
traverseDir(filePath);
} else if (file.endsWith(".json")) {
// 如果是 .json 文件,进行检查
checkJsonFile(filePath);
}
});
}
// 遍历 JSON 内部的 key替换内容
function traverseJson(obj, path) {
const result = {
replace: false,
obj: Array.isArray(obj) ? [] : {}
};
path = path !== undefined ? path : "";
for (let key in obj) {
if (key === "result" || key === "results" || key === "output"){
result.obj[key] = obj[key];
continue;
}
if (typeof obj[key] === "object") {
const returnedResult = traverseJson(obj[key], path + "." + key);
result.replace = result.replace || returnedResult.replace;
result.obj[key] = returnedResult.obj;
continue;
}
if (key === "item"){
// Search item
if (settings.enableSearch && settings.search && settings.search.items && settings.search.items.includes(obj[key])){
console.log(`[Item Found] ${path}.${key} 包含:${obj[key]}`);
}
// Replace
if (settings.same){
const index = settings.same.findIndex((v) => v.list.includes(obj[key]));
if (index >= 0){
console.log(`[Replace] ${path}.${key} 包含:${obj[key]}`);
result.replace = true;
result.obj["tag"] = settings.same[index].tag;
continue;
}
}
} else if (key === "tag"){
// Search tag
if (settings.enableSearch && settings.search && settings.search.tags && settings.search.tags.includes(obj[key])){
console.log(`[Key Found] ${path}.${key} 包含:${obj[key]}`);
}
}
result.obj[key] = obj[key];
}
return result;
}
// 检查每个 .json 文件是否包含 "ingredients" 属性
function checkJsonFile(filePath) {
try {
const recipeJson = JSON.parse(fs.readFileSync(filePath, "utf8"));
// if (!recipeJson["ingredients"] && !recipeJson["ingredient"] && !recipeJson["key"]) {
// console.log(`未找到 "ingredients" 属性的文件: ${filePath}`);
// }
if (
recipeJson["result"] ||
recipeJson["results"] ||
recipeJson["output"]
) {
// console.log(`文件: ${filePath}`);
const result = traverseJson(recipeJson, `${filePath}$`);
if (settings.enableGenerate && result.replace){
const writeFilePath = path.join(recipesDir, filePath);
console.log(`[Replace] 正在写入文件:${writeFilePath}`);
createDirIfNotExist(path.dirname(writeFilePath));
fs.writeFileSync(writeFilePath, JSON.stringify(result.obj, null, 2));
}
} else {
// console.log(`未找到 输出 属性的文件: ${filePath}`);
}
} catch (err) {
console.error(`无法读取或解析文件: ${filePath}`);
console.error(err);
}
}
function generateTags(){
const recipesDataDir = path.join(recipesDir, "data");
createDirIfNotExist(recipesDir); // 确保 ./recipes 文件夹存在
writePackMcmeta(); // 确保 ./recipes/pack.mcmeta 文件存在
if (fs.existsSync(recipesDataDir)) {
console.log(`正在清理 "${recipesDataDir}"`);
fs.rmSync(recipesDataDir, { recursive: true, force: true });
}
if (settings.same){
settings.same.forEach((ele) => {
const splitedTag = ele.tag.split(":");
if (splitedTag.length !== 2){
console.error(`"${ele.tag}"格式不正确`);
}
const namespace = splitedTag[0];
const tagName = splitedTag[1];
const filePath = path.join(recipesDataDir, namespace, "tags/items/", tagName + ".json");
console.log(`正在写入文件:${filePath}`);
createDirIfNotExist(path.dirname(filePath));
fs.writeFileSync(filePath, JSON.stringify({
replace: ele.replace,
values: ele.list
}, null, 2));
});
}
}
// 从 ./data 目录开始遍历
const dataDir = "./data";
if (settings.enableGenerate){
generateTags();
}
traverseDir(dataDir);

12
generate.js.txt Normal file
View File

@ -0,0 +1,12 @@
按照此伪代码编写一个Node.js脚本要求
1. 尽量不使用第三方库adm-zip 除外用来解析jar包
2. 使用双引号代替单引号
3. 使用引号
伪代码:
递归遍历 ./data 及其子文件夹下的所有 *.json {
const recipeJson = 读取 *.json
if (!recipeJson["ingredients"]){
console.log(*.json文件路径)
}
}

27
package-lock.json generated Normal file
View File

@ -0,0 +1,27 @@
{
"name": "Minecraft Mod Translator",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"adm-zip": "^0.5.16"
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"engines": {
"node": ">=12.0"
}
}
},
"dependencies": {
"adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"adm-zip": "^0.5.16"
}
}

53
settings.json Normal file
View File

@ -0,0 +1,53 @@
{
"enableSearch": false,
"enableGenerate": true,
"same": [
{
"tag": "c:crops/coffee_beans",
"replace": false,
"list": [
"croptopia:coffee_beans",
"farmersrespite:coffee_beans",
"coffee_delight:coffee_beans"
]
},
{
"tag": "c:coffee_beans",
"replace": false,
"list": [
"croptopia:coffee_beans",
"farmersrespite:coffee_beans",
"coffee_delight:coffee_beans"
]
},
{
"tag": "c:lettuce",
"replace": false,
"list": [
"culinaire:lettuce",
"croptopia:lettuce"
]
},
{
"tag": "c:vegetables/cabbage",
"replace": false,
"list": [
"farmersdelight:cabbage",
"croptopia:cabbage"
]
}
],
"search": {
"tags": [
"c:crops/coffee_beans",
"c:coffee_beans",
"c:coffees"
],
"items": [
"croptopia:coffee_beans",
"farmersrespite:coffee_beans",
"farmersdelight:cabbage",
"farmersdelight:cabbage_leaf"
]
}
}