First commit
This commit is contained in:
commit
30a4c17d79
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
data
|
||||
mods
|
||||
node_modules
|
||||
recipes
|
||||
recipes.zip
|
44
README.md
Normal file
44
README.md
Normal 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
101
extract_recipes.js
Normal 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
15
extract_recipes.txt
Normal 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
163
generate.js
Normal 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
12
generate.js.txt
Normal 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
27
package-lock.json
generated
Normal 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
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16"
|
||||
}
|
||||
}
|
53
settings.json
Normal file
53
settings.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user