mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-09-02 20:33:11 +02:00
Merge branch 'main' into default-behaviors
This commit is contained in:
commit
7e80efb5b4
12 changed files with 169 additions and 48 deletions
35
README.md
35
README.md
|
@ -1,20 +1,41 @@
|
||||||
# Mindcraft
|
# Mindcraft
|
||||||
|
|
||||||
Crafting minds for Minecraft with AI!
|
Crafting minds for Minecraft with ChatGPT and Mineflayer
|
||||||
|
|
||||||
|
#### ‼️Warning‼️
|
||||||
|
|
||||||
|
This project allows an AI model to write/execute code on your computer that may be insecure, dangerous, and vulnerable to injection attacks on public servers. Code writing is disabled by default, you can enable it by setting `allow_insecure_coding` to `true` in `settings.json`. Enable only on local or private servers, **never** on public servers. Ye be warned.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [OpenAI API Subscription](https://openai.com/blog/openai-api)
|
||||||
|
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (at most v1.20.2)
|
||||||
|
- [Node.js](https://nodejs.org/) (at least v14)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install Node.js >= 14 from [nodejs.org](https://nodejs.org/)
|
Add `OPENAI_API_KEY` (and optionally `OPENAI_ORG_ID`) to your environment variables
|
||||||
|
|
||||||
Install node modules with `npm install`
|
Clone/Download this repository
|
||||||
|
|
||||||
## Usage
|
Run `npm install`
|
||||||
|
|
||||||
Start minecraft server on localhost port `55916`
|
## Run
|
||||||
|
|
||||||
Add `OPENAI_API_KEY` (and optionally `OPENAI_ORG_ID`) to your environment variables.
|
Start a minecraft world and open it to LAN on localhost port `55916`
|
||||||
|
|
||||||
run `node main.js`
|
Run `node main.js`
|
||||||
|
|
||||||
|
You can configure details in `settings.json`. Here is an example settings for connecting to a non-local server:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"minecraft_version": "1.20.1",
|
||||||
|
"host": "111.222.333.444",
|
||||||
|
"port": 55920,
|
||||||
|
"auth": "microsoft",
|
||||||
|
"allow_insecure_coding": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Patches
|
## Patches
|
||||||
|
|
||||||
|
|
7
settings.json
Normal file
7
settings.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"minecraft_version": "1.20.1",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 55916,
|
||||||
|
"auth": "offline",
|
||||||
|
"allow_insecure_coding": true
|
||||||
|
}
|
|
@ -14,16 +14,18 @@ export class Agent {
|
||||||
this.history = new History(this);
|
this.history = new History(this);
|
||||||
this.coder = new Coder(this);
|
this.coder = new Coder(this);
|
||||||
|
|
||||||
|
console.log('Loading examples...');
|
||||||
|
|
||||||
this.history.load(profile);
|
this.history.load(profile);
|
||||||
await this.examples.load('./src/examples.json');
|
await this.examples.load('./src/examples.json');
|
||||||
await this.coder.load();
|
await this.coder.load();
|
||||||
|
|
||||||
|
console.log('Logging in...');
|
||||||
this.bot = initBot(name);
|
this.bot = initBot(name);
|
||||||
|
|
||||||
initModes(this);
|
initModes(this);
|
||||||
|
|
||||||
this.bot.on('login', async () => {
|
this.bot.on('login', async () => {
|
||||||
|
|
||||||
console.log(`${this.name} logged in.`);
|
console.log(`${this.name} logged in.`);
|
||||||
this.coder.clear();
|
this.coder.clear();
|
||||||
|
|
||||||
|
|
|
@ -228,13 +228,17 @@ export class Coder {
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
if (!this.executing) return;
|
if (!this.executing) return;
|
||||||
|
const start = Date.now();
|
||||||
while (this.executing) {
|
while (this.executing) {
|
||||||
this.agent.bot.interrupt_code = true;
|
this.agent.bot.interrupt_code = true;
|
||||||
this.agent.bot.collectBlock.cancelTask();
|
this.agent.bot.collectBlock.cancelTask();
|
||||||
this.agent.bot.pathfinder.stop();
|
this.agent.bot.pathfinder.stop();
|
||||||
this.agent.bot.pvp.stop();
|
this.agent.bot.pvp.stop();
|
||||||
console.log('waiting for code to finish executing... interrupt:', this.agent.bot.interrupt_code);
|
console.log('waiting for code to finish executing...');
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
if (Date.now() - start > 10 * 1000) {
|
||||||
|
process.exit(1); // force exit program after 10 seconds of failing to stop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as skills from '../library/skills.js';
|
import * as skills from '../library/skills.js';
|
||||||
|
import settings from '../../settings.js';
|
||||||
|
|
||||||
function wrapExecution(func, timeout=-1, resume_name=null) {
|
function wrapExecution(func, timeout=-1, resume_name=null) {
|
||||||
return async function (agent, ...args) {
|
return async function (agent, ...args) {
|
||||||
|
@ -24,6 +24,8 @@ export const actionsList = [
|
||||||
name: '!newAction',
|
name: '!newAction',
|
||||||
description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.',
|
description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.',
|
||||||
perform: async function (agent) {
|
perform: async function (agent) {
|
||||||
|
if (!settings.allow_insecure_coding)
|
||||||
|
return 'Agent is not allowed to write code.';
|
||||||
await agent.coder.generateCode(agent.history);
|
await agent.coder.generateCode(agent.history);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -86,6 +88,14 @@ export const actionsList = [
|
||||||
await skills.followPlayer(agent.bot, player_name);
|
await skills.followPlayer(agent.bot, player_name);
|
||||||
}, -1, 'followPlayer')
|
}, -1, 'followPlayer')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '!moveAway',
|
||||||
|
description: 'Move away from the current location in any direction by a given distance. Ex: !moveAway(2)',
|
||||||
|
params: {'distance': '(number) The distance to move away.'},
|
||||||
|
perform: wrapExecution(async (agent, distance) => {
|
||||||
|
await skills.moveAway(agent.bot, distance);
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '!givePlayer',
|
name: '!givePlayer',
|
||||||
description: 'Give the specified item to the given player. Ex: !givePlayer("steve", "stone_pickaxe", 1)',
|
description: 'Give the specified item to the given player. Ex: !givePlayer("steve", "stone_pickaxe", 1)',
|
||||||
|
@ -155,5 +165,12 @@ export const actionsList = [
|
||||||
perform: wrapExecution(async (agent) => {
|
perform: wrapExecution(async (agent) => {
|
||||||
await skills.goToBed(agent.bot);
|
await skills.goToBed(agent.bot);
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '!stay',
|
||||||
|
description: 'Stay in the current location no matter what. Pauses all modes.',
|
||||||
|
perform: wrapExecution(async (agent) => {
|
||||||
|
await skills.stay(agent.bot);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -273,12 +273,12 @@ export async function attackEntity(bot, entity, kill=true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log(bot, `Successfully killed ${entity.name}.`);
|
log(bot, `Successfully killed ${entity.name}.`);
|
||||||
await pickupNearbyItem(bot);
|
await pickupNearbyItems(bot);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function defendSelf(bot, range=8) {
|
export async function defendSelf(bot, range=9) {
|
||||||
/**
|
/**
|
||||||
* Defend yourself from all nearby hostile mobs until there are no more.
|
* Defend yourself from all nearby hostile mobs until there are no more.
|
||||||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||||
|
@ -336,24 +336,27 @@ export async function collectBlock(bot, blockType, num=1) {
|
||||||
blocktypes.push('deepslate_'+blockType);
|
blocktypes.push('deepslate_'+blockType);
|
||||||
|
|
||||||
let collected = 0;
|
let collected = 0;
|
||||||
const blocks = world.getNearestBlocks(bot, blocktypes, 64, num);
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
log(bot, `Could not find any ${blockType} to collect.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const first_block = blocks[0];
|
|
||||||
await bot.tool.equipForBlock(first_block);
|
|
||||||
const itemId = bot.heldItem ? bot.heldItem.type : null
|
|
||||||
if (!first_block.canHarvest(itemId)) {
|
|
||||||
log(bot, `Don't have right tools to harvest ${blockType}.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let block of blocks) {
|
for (let i=0; i<num; i++) {
|
||||||
|
const blocks = world.getNearestBlocks(bot, blocktypes, 64, 1);
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
if (collected === 0)
|
||||||
|
log(bot, `No ${blockType} nearby to collect.`);
|
||||||
|
else
|
||||||
|
log(bot, `No more ${blockType} nearby to collect.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const block = blocks[0];
|
||||||
|
await bot.tool.equipForBlock(block);
|
||||||
|
const itemId = bot.heldItem ? bot.heldItem.type : null
|
||||||
|
if (!block.canHarvest(itemId)) {
|
||||||
|
log(bot, `Don't have right tools to harvest ${blockType}.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await bot.collectBlock.collect(block);
|
await bot.collectBlock.collect(block);
|
||||||
collected++;
|
collected++;
|
||||||
autoLight(bot);
|
await autoLight(bot);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
if (err.name === 'NoChests') {
|
if (err.name === 'NoChests') {
|
||||||
|
@ -379,7 +382,7 @@ export async function pickupNearbyItems(bot) {
|
||||||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||||
* @returns {Promise<boolean>} true if the items were picked up, false otherwise.
|
* @returns {Promise<boolean>} true if the items were picked up, false otherwise.
|
||||||
* @example
|
* @example
|
||||||
* await skills.pickupNearbyItem(bot);
|
* await skills.pickupNearbyItems(bot);
|
||||||
**/
|
**/
|
||||||
const distance = 8;
|
const distance = 8;
|
||||||
const getNearestItem = bot => bot.nearestEntity(entity => entity.name === 'item' && bot.entity.position.distanceTo(entity.position) < distance);
|
const getNearestItem = bot => bot.nearestEntity(entity => entity.name === 'item' && bot.entity.position.distanceTo(entity.position) < distance);
|
||||||
|
@ -413,9 +416,24 @@ export async function breakBlockAt(bot, x, y, z) {
|
||||||
* let position = world.getPosition(bot);
|
* let position = world.getPosition(bot);
|
||||||
* await skills.breakBlockAt(bot, position.x, position.y - 1, position.x);
|
* await skills.breakBlockAt(bot, position.x, position.y - 1, position.x);
|
||||||
**/
|
**/
|
||||||
let current = bot.blockAt(Vec3(x, y, z));
|
let block = bot.blockAt(Vec3(x, y, z));
|
||||||
if (current.name != 'air')
|
if (block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') {
|
||||||
await bot.dig(current, true);
|
await bot.tool.equipForBlock(block);
|
||||||
|
const itemId = bot.heldItem ? bot.heldItem.type : null
|
||||||
|
if (!block.canHarvest(itemId)) {
|
||||||
|
log(bot, `Don't have right tools to break ${block.name}.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bot.entity.position.distanceTo(block.position) > 4.5) {
|
||||||
|
let pos = block.position;
|
||||||
|
let movements = new pf.Movements(bot);
|
||||||
|
movements.canPlaceOn = false;
|
||||||
|
movements.allow1by1towers = false;
|
||||||
|
bot.pathfinder.setMovements();
|
||||||
|
await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4));
|
||||||
|
}
|
||||||
|
await bot.dig(block, true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -663,7 +681,45 @@ export async function followPlayer(bot, username) {
|
||||||
log(bot, `You are now actively following player ${username}.`);
|
log(bot, `You are now actively following player ${username}.`);
|
||||||
|
|
||||||
while (!bot.interrupt_code) {
|
while (!bot.interrupt_code) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function moveAway(bot, distance) {
|
||||||
|
/**
|
||||||
|
* Move away from current position in any direction.
|
||||||
|
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||||
|
* @param {number} distance, the distance to move away.
|
||||||
|
* @returns {Promise<boolean>} true if the bot moved away, false otherwise.
|
||||||
|
* @example
|
||||||
|
* await skills.moveAway(bot, 8);
|
||||||
|
**/
|
||||||
|
const pos = bot.entity.position;
|
||||||
|
let goal = new pf.goals.GoalNear(pos.x, pos.y, pos.z, distance);
|
||||||
|
let inverted_goal = new pf.goals.GoalInvert(goal);
|
||||||
|
bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||||
|
await bot.pathfinder.goto(inverted_goal);
|
||||||
|
let new_pos = bot.entity.position;
|
||||||
|
log(bot, `Moved away from nearest entity to ${new_pos}.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stay(bot) {
|
||||||
|
/**
|
||||||
|
* Stay in the current position until interrupted. Disables all modes.
|
||||||
|
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||||
|
* @returns {Promise<boolean>} true if the bot stayed, false otherwise.
|
||||||
|
* @example
|
||||||
|
* await skills.stay(bot);
|
||||||
|
**/
|
||||||
|
bot.modes.pause('self_defense');
|
||||||
|
bot.modes.pause('hunting');
|
||||||
|
bot.modes.pause('torch_placing');
|
||||||
|
bot.modes.pause('item_collecting');
|
||||||
|
while (!bot.interrupt_code) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import pf from 'mineflayer-pathfinder';
|
import pf from 'mineflayer-pathfinder';
|
||||||
import { getAllBlockIds } from '../../utils/mcdata.js';
|
import { getAllBlockIds, getAllBiomes } from '../../utils/mcdata.js';
|
||||||
|
|
||||||
|
|
||||||
export function getNearestFreeSpace(bot, size=1, distance=8) {
|
export function getNearestFreeSpace(bot, size=1, distance=8) {
|
||||||
|
@ -271,5 +271,5 @@ export function getBiomeName(bot) {
|
||||||
* let biome = world.getBiomeName(bot);
|
* let biome = world.getBiomeName(bot);
|
||||||
**/
|
**/
|
||||||
const biomeId = bot.world.getBiome(bot.entity.position);
|
const biomeId = bot.world.getBiome(bot.entity.position);
|
||||||
return mcdata.biomes[biomeId].name;
|
return getAllBiomes()[biomeId].name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,22 +55,25 @@ const modes = [
|
||||||
active: false,
|
active: false,
|
||||||
|
|
||||||
wait: 2, // number of seconds to wait after noticing an item to pick it up
|
wait: 2, // number of seconds to wait after noticing an item to pick it up
|
||||||
noticedAt: -1,
|
prev_item: null,
|
||||||
|
noticed_at: -1,
|
||||||
update: async function (agent) {
|
update: async function (agent) {
|
||||||
let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8);
|
let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8);
|
||||||
if (item && await world.isClearPath(agent.bot, item)) {
|
if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item)) {
|
||||||
if (this.noticedAt === -1) {
|
if (this.noticed_at === -1) {
|
||||||
this.noticedAt = Date.now();
|
this.noticed_at = Date.now();
|
||||||
}
|
}
|
||||||
if (Date.now() - this.noticedAt > this.wait * 1000) {
|
if (Date.now() - this.noticed_at > this.wait * 1000) {
|
||||||
agent.bot.chat(`Picking up ${item.name}!`);
|
agent.bot.chat(`Picking up ${item.name}!`);
|
||||||
|
this.prev_item = item;
|
||||||
execute(this, agent, async () => {
|
execute(this, agent, async () => {
|
||||||
await skills.pickupNearbyItems(agent.bot);
|
await skills.pickupNearbyItems(agent.bot);
|
||||||
});
|
});
|
||||||
|
this.noticed_at = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.noticedAt = -1;
|
this.noticed_at = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,6 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{"role": "user", "content": "brug: build a dirt house"},
|
{"role": "user", "content": "brug: build a dirt house"},
|
||||||
{"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n// place a torch in the middle of the room\nawait skills.placeBlock(bot, 'torch', startX + width / 2, startY + height / 2, startZ + 1);```"}
|
{"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n```"}
|
||||||
]
|
]
|
||||||
]
|
]
|
3
src/settings.js
Normal file
3
src/settings.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
const settings = JSON.parse(readFileSync('./settings.json', 'utf8'));
|
||||||
|
export default settings;
|
|
@ -7,11 +7,17 @@ if (process.env.OPENAI_ORG_ID) {
|
||||||
organization: process.env.OPENAI_ORG_ID,
|
organization: process.env.OPENAI_ORG_ID,
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
else if (process.env.OPENAI_API_KEY) {
|
||||||
openAiConfig = {
|
openAiConfig = {
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
console.error('OpenAI API key missing! Make sure you set OPENAI_API_KEY and OPENAI_ORG_ID (optional) environment variables.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const openai = new OpenAIApi(openAiConfig);
|
const openai = new OpenAIApi(openAiConfig);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import minecraftData from 'minecraft-data';
|
import minecraftData from 'minecraft-data';
|
||||||
|
import settings from '../settings.js';
|
||||||
import { createBot } from 'mineflayer';
|
import { createBot } from 'mineflayer';
|
||||||
import { pathfinder } from 'mineflayer-pathfinder';
|
import { pathfinder } from 'mineflayer-pathfinder';
|
||||||
import { plugin as pvp } from 'mineflayer-pvp';
|
import { plugin as pvp } from 'mineflayer-pvp';
|
||||||
|
@ -7,7 +8,7 @@ import { plugin as autoEat } from 'mineflayer-auto-eat';
|
||||||
import plugin from 'mineflayer-armor-manager';
|
import plugin from 'mineflayer-armor-manager';
|
||||||
const armorManager = plugin;
|
const armorManager = plugin;
|
||||||
|
|
||||||
const mc_version = '1.20.1'
|
const mc_version = settings.minecraft_version;
|
||||||
const mcdata = minecraftData(mc_version);
|
const mcdata = minecraftData(mc_version);
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,12 +16,9 @@ export function initBot(username) {
|
||||||
let bot = createBot({
|
let bot = createBot({
|
||||||
username: username,
|
username: username,
|
||||||
|
|
||||||
host: 'localhost',
|
host: settings.host,
|
||||||
port: 55916,
|
port: settings.port,
|
||||||
|
auth: settings.auth,
|
||||||
// host: '000.111.222.333',
|
|
||||||
// port: 55920,
|
|
||||||
// auth: 'microsoft',
|
|
||||||
|
|
||||||
version: mc_version,
|
version: mc_version,
|
||||||
});
|
});
|
||||||
|
@ -97,3 +95,7 @@ export function getAllBlockIds(ignore) {
|
||||||
}
|
}
|
||||||
return blockIds;
|
return blockIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllBiomes() {
|
||||||
|
return mcdata.biomes;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue