├── babel.config.js ├── src ├── Pillar.js ├── PositionContext.js ├── components │ ├── Container.js │ ├── Canvas.js │ └── Block.js ├── index.js ├── ReactMinecraft.js └── House.js ├── .gitignore ├── package.json └── README.md /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/preset-react" 4 | ] 5 | ]; 6 | 7 | export default {presets}; 8 | 9 | -------------------------------------------------------------------------------- /src/Pillar.js: -------------------------------------------------------------------------------- 1 | import { Block } from "./components/Block.js"; 2 | import React from "react"; 3 | import { Container } from "./components/Container.js"; 4 | 5 | export default function Pillar({name, x, y, z, height = 5}) { 6 | let content = []; 7 | for (let i = 0; i < height; i++) { 8 | content.push(); 9 | } 10 | return ({content}); 11 | } -------------------------------------------------------------------------------- /src/PositionContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const PositionContext = createContext(null); 4 | 5 | export const addPosition = (position, x, y, z) => { 6 | if(position === null){ 7 | return { 8 | x: x, 9 | y: y, 10 | z: z 11 | } 12 | } 13 | return { 14 | x: position.x + x, 15 | y: position.y + y, 16 | z: position.z + z 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/Container.js: -------------------------------------------------------------------------------- 1 | import { addPosition, PositionContext } from "../PositionContext.js"; 2 | import React from "react"; 3 | 4 | export function Container({ children, x, y, z }) { 5 | const position = React.useContext(PositionContext) 6 | console.log('context position in container', position); 7 | const absolutePosition = addPosition(position, x, y, z) 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | .idea 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | *.original 39 | 40 | world 41 | logs 42 | lib -------------------------------------------------------------------------------- /src/components/Canvas.js: -------------------------------------------------------------------------------- 1 | import { addPosition, PositionContext } from "../PositionContext.js"; 2 | import React from "react"; 3 | 4 | export function Canvas({ children, x, y, z, xSpan, ySpan, zSpan }) { 5 | const position = React.useContext(PositionContext) 6 | const absolutePosition = addPosition(position, x, y, z) 7 | return ( 8 | <> 9 | 10 | 11 | {children} 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-minecraft", 3 | "version": "1.0.0", 4 | "description": "A React Implementation for Minecraft", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/babel src --out-dir lib", 8 | "start": "DEBUG=\"minecraft-protocol\" ./node_modules/.bin/babel src --out-dir lib && node lib/index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "reset-world": "rm -rf world && mkdir world" 11 | }, 12 | "author": "Bufferhead (Gregor Vostrak)", 13 | "license": "MIT", 14 | "type": "module", 15 | "dependencies": { 16 | "@babel/preset-react": "^7.22.15", 17 | "flying-squid": "^1.5.0", 18 | "minecraft-data": "^3.48.0", 19 | "prismarine-block": "^1.17.1", 20 | "prismarine-registry": "^1.7.0", 21 | "react": "^18.2.0", 22 | "react-reconciler": "^0.29.0", 23 | "vec3": "^0.1.8" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "^7.23.0", 27 | "@babel/core": "^7.23.2", 28 | "@babel/preset-env": "^7.23.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import House from "./House.js"; 2 | 3 | import * as mcServer from 'flying-squid' 4 | import ReactMinecraft from './ReactMinecraft.js' 5 | 6 | const server = mcServer.createMCServer({ 7 | 'motd': 'A Minecraft Server \nRunning flying-squid', 8 | 'port': 25565, 9 | 'max-players': 10, 10 | 'online-mode': true, 11 | 'logging': true, 12 | 'gameMode': 1, 13 | 'difficulty': 1, 14 | 'worldFolder': 'world', 15 | 'generation': { 16 | 'name': 'superflat', 17 | 'options': { 18 | 'worldHeight': 80 19 | } 20 | }, 21 | 'kickTimeout': 10000, 22 | 'plugins': {}, 23 | 'modpe': false, 24 | 'view-distance': 10, 25 | 'player-list-text': { 26 | 'header': 'Flying squid', 27 | 'footer': 'Test server' 28 | }, 29 | 'everybody-op': true, 30 | 'max-entities': 100, 31 | 'version': '1.16.1' 32 | }) 33 | 34 | server.addPlugin('random', { 35 | server: function (serv) { 36 | serv.commands.add({ 37 | base: 'reload', // This is what the user starts with, so in this case: /random 38 | info: 'Reloads the current React Structure', // Description of the command 39 | usage: '/reload', // Usage displayed if parse() returns false (which means they used it incorrectly) 40 | // parse(str) { // str contains everything after "/random " 41 | // const match = str.match(/^\d+$/); // Check to see if they put numbers in a row 42 | // if (!match) return false; // Anything else, show them the usage 43 | // else return parseInt(match[0]); // Otherwise, pass our number as an int to action() 44 | // }, 45 | action(maxNumber, ctx) { // ctx - context who is using it 46 | ReactMinecraft.render(House(), {server: serv, world: ctx.player.world}, () => {console.log('done')}); 47 | } 48 | }) 49 | } 50 | } 51 | ) 52 | 53 | export default {}; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Minecraft 2 | 3 | React Minecraft is a project that allows you to create Minecraft worlds using React components. 4 | Just like React-DOM and React-Native, React Minecraft uses the React.js Framework and JSX to craft custom builds in Minecraft. With Minecraft Blocks. 5 | 6 | **You can watch how i made it [here](http://www.youtube.com/watch?v=YaX5ZAEqXD8)** 7 | 8 | [![Youtube Video about how this project was made](http://img.youtube.com/vi/YaX5ZAEqXD8/0.jpg)](http://www.youtube.com/watch?v=YaX5ZAEqXD8 "I built a Minecraft house using React.js") 9 | 10 | An Example for the Top Layer Component for a Minecraft house could look like this: 11 | 12 | ```javascript 13 | export default function House() { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | ``` 29 | 30 | ## Run the project 31 | 32 | To run the project type `npm run start`. 33 | This will start a local minecraft server on port 25565. 34 | You can connect to this server using the Minecraft Java Edition via localhost:25565. 35 | 36 | Currently only Minecraft Version **v1.16.1** is supported. 37 | 38 | ## How to use 39 | 40 | After logging in you can type `/reload` to reload the component. 41 | 42 | ## State of this project 43 | 44 | This is a very early release of this project (you could call it a prototype), the API is still subject to change. 45 | Depending on the Feedback this project will be further developed to provide a stable API. 46 | 47 | ## Minecraft Block Index + Block States 48 | 49 | For documentation of blocks and available states 50 | 51 | https://minecraftitemids.com/ 52 | 53 | ## Roadmap / Ideas: 54 | 55 | * Add better component reloading (after file updates) without server restart 56 | * Add rotation and mirror possibility for blocks/sections 57 | * Probably make it faster, not really sure how well it performs on larger structures yet 58 | * Check if we can make events (f.e. button press) work 59 | * If events work, add reactivity 60 | * If reactivity works see if functions can work 61 | 62 | ## Contributing 63 | 64 | If you are interested in contributing feel free to either contact me or open tickets for what you would like to contribute. 65 | Please don't open PRs for new features without prior discussion. Bugfixes and small changes are of course welcome without prior ticket. -------------------------------------------------------------------------------- /src/components/Block.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { addPosition, PositionContext } from "../PositionContext.js"; 3 | import minecraftData from "minecraft-data"; 4 | import prismarineRegistry from 'prismarine-registry' 5 | import prismarineBlock from 'prismarine-block' 6 | 7 | export function Block({ 8 | children, 9 | x, 10 | y, 11 | z, 12 | id, 13 | name, 14 | facing = 'north', 15 | waterlogged = false, 16 | half = 'bottom', 17 | axis = 'x', 18 | east = false, 19 | west = false, 20 | north = false, 21 | south = false, 22 | type = 'top', 23 | snowy = false, 24 | open = true 25 | }) { 26 | const mcData = minecraftData('1.16.1') 27 | 28 | const registry = prismarineRegistry('1.16.1') 29 | const Block = prismarineBlock(registry) 30 | const properties = {}; 31 | const currentBlock = Block.fromString(name, registry.biomesByName.plains.id); 32 | console.log(currentBlock); 33 | const avaliableProperties = currentBlock.getProperties(); 34 | // TODO: Refactor to be more generic, this is a lot of repeated code 35 | if (avaliableProperties.hasOwnProperty('facing')) { 36 | properties.facing = facing; 37 | } 38 | if (avaliableProperties.hasOwnProperty('waterlogged')) { 39 | properties.waterlogged = waterlogged; 40 | } 41 | if (avaliableProperties.hasOwnProperty('half')) { 42 | // TODO: the default for doors is not working here because they use "upper" and "lower", 43 | // so for now it needs to be manually overridden, otherwise weird stuff happens with doors 44 | properties.half = half; 45 | } 46 | if (avaliableProperties.hasOwnProperty('axis')) { 47 | properties.axis = axis; 48 | } 49 | if (avaliableProperties.hasOwnProperty('east')) { 50 | properties.east = east; 51 | } 52 | if (avaliableProperties.hasOwnProperty('west')) { 53 | properties.west = west; 54 | } 55 | if (avaliableProperties.hasOwnProperty('north')) { 56 | properties.north = north; 57 | } 58 | if (avaliableProperties.hasOwnProperty('south')) { 59 | properties.south = south; 60 | } 61 | if (avaliableProperties.hasOwnProperty('type')) { 62 | properties.type = type; 63 | } 64 | if (avaliableProperties.hasOwnProperty('snowy')) { 65 | properties.snowy = snowy; 66 | } 67 | if (avaliableProperties.hasOwnProperty('open')) { 68 | properties.open = open; 69 | } 70 | 71 | 72 | 73 | 74 | 75 | const newblock = Block.fromProperties(name, properties, registry.biomesByName.plains.id); 76 | const state = newblock.stateId; 77 | 78 | const position = useContext(PositionContext) 79 | const absolutePosition = addPosition(position, x, y, z) 80 | 81 | return () 83 | } 84 | -------------------------------------------------------------------------------- /src/ReactMinecraft.js: -------------------------------------------------------------------------------- 1 | import { Vec3 } from "vec3"; 2 | import { DefaultEventPriority, } from 'react-reconciler/constants.js'; 3 | 4 | import Reconciler from 'react-reconciler'; 5 | 6 | // Documentation: https://github.com/facebook/react/tree/main/packages/react-reconciler#shouldsettextcontenttype-props 7 | const HostConfig = { 8 | supportsMutation: true, 9 | createInstance(type, props, rootContainer, hostContext, internalHandle) { 10 | if (type === 'nativeminecraftblock') { 11 | return {position: new Vec3(props.x, props.y, props.z), id: props.id}; 12 | } 13 | else if(type === 'nativeminecraftcanvas') { 14 | // TODO: there is probably a better way to achieve this 15 | console.log('create canvas', props); 16 | rootContainer.canvas = props; 17 | } 18 | }, 19 | createTextInstance(text, rootContainer, hostContext, internalHandle) { 20 | throw new Error('Text is not supported'); 21 | }, 22 | // appendInitialChild(parentInstance, child) -> i think i dont need this ? appends child to parentInstance 23 | finalizeInitialChildren(instance, type, props, rootContainer, hostContext) { 24 | return false; 25 | }, 26 | shouldSetTextContent(type, props) { 27 | return false; 28 | }, 29 | getRootHostContext(rootContainer) { 30 | return null; 31 | }, 32 | getChildHostContext(parentHostContext, type, rootContainer) { 33 | return parentHostContext; 34 | }, 35 | getPublicInstance(instance) { 36 | return instance; 37 | }, 38 | prepareForCommit(containerInfo) { 39 | return null; 40 | }, 41 | resetAfterCommit(containerInfo) { 42 | 43 | }, 44 | preparePortalMount(containerInfo) { 45 | 46 | }, 47 | scheduleTimeout(fn, delay) { 48 | setTimeout(fn, delay); 49 | }, 50 | cancelTimeout(id) { 51 | clearTimeout(id); 52 | }, 53 | noTimeout: -1, 54 | supportsMicrotasks: true, 55 | scheduleMicrotask(fn) { 56 | queueMicrotask(fn); 57 | }, 58 | isPrimaryRenderer: true, 59 | getCurrentEventPriority() { 60 | return DefaultEventPriority; 61 | }, 62 | clearContainer(container) { 63 | console.log('clear container'); 64 | if(container.hasOwnProperty('canvas') && container.canvas){ 65 | for (let x = container.canvas.x; x < (container.canvas.x + container.canvas.xSpan); x++) { 66 | for (let y = container.canvas.y; y < (container.canvas.y + container.canvas.ySpan); y++) { 67 | for (let z = container.canvas.z; z < (container.canvas.z + container.canvas.zSpan); z++) { 68 | // Reset to Air Blocks 69 | container.server.setBlock(container.world, new Vec3(x, y, z), 0); 70 | } 71 | } 72 | } 73 | } 74 | else{ 75 | console.log('did not find canvas, skip reset'); 76 | } 77 | }, 78 | appendChild(parentInstance, child) { 79 | console.log('append child'); 80 | console.log(parentInstance, child); 81 | }, 82 | appendChildToContainer(container, child) { 83 | // TODO: might want to skip blocks here that are outside of the canvas 84 | if(child && child.hasOwnProperty('position') && child.position){ 85 | container.server.setBlock(container.world, child.position, child.id); 86 | } 87 | console.log('append child to container'); 88 | console.log(child); 89 | }, 90 | insertBefore(parentInstance, child, beforeChild) { 91 | console.log('insert before'); 92 | console.log(parentInstance, child, beforeChild); 93 | }, 94 | insertInContainerBefore(container, child, beforeChild) { 95 | console.log('insert in container before'); 96 | console.log(container, child, beforeChild); 97 | }, 98 | removeChild(parentInstance, child) { 99 | console.log('remove child'); 100 | console.log(parentInstance, child); 101 | }, 102 | removeChildFromContainer(container, child) { 103 | console.log('remove child from container'); 104 | console.log(container, child); 105 | }, 106 | resetTextContent(instance) { 107 | }, 108 | commitTextUpdate(textInstance, oldText, newText) { 109 | console.log('commit text update'); 110 | console.log(textInstance, oldText, newText); 111 | }, 112 | commitMount(instance, type, newProps, internalInstanceHandle) { 113 | }, 114 | commitUpdate(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle) { 115 | console.log('commit update'); 116 | console.log(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle); 117 | }, 118 | hideInstance(instance) { 119 | }, 120 | hideTextInstance(textInstance) { 121 | }, 122 | unhideInstance(instance, props) { 123 | }, 124 | unhideTextInstance(textInstance, text) { 125 | }, 126 | } 127 | 128 | const MyRenderer = Reconciler(HostConfig); 129 | 130 | const RendererPublicAPI = { 131 | render(element, container, callback) { 132 | MyRenderer.updateContainer(element, MyRenderer.createContainer(container), null, callback); 133 | } 134 | }; 135 | 136 | export default RendererPublicAPI; 137 | -------------------------------------------------------------------------------- /src/House.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Canvas } from "./components/Canvas.js"; 3 | import Pillar from "./Pillar.js"; 4 | import { Block } from "./components/Block.js"; 5 | import { Container } from "./components/Container.js"; 6 | 7 | function FrontWall() { 8 | return ( 9 | <> 10 | {[...Array(5)].map((x, i) => 11 | 12 | )} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | function BackWall() { 30 | return ( 31 | <> 32 | {[...Array(5)].map((x, i) => 33 | 34 | )} 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | function SideWall({x, y, z}) { 44 | return ( 45 | 46 | {[...Array(6)].map((x, i) => 47 | 48 | )} 49 | 50 | ) 51 | } 52 | 53 | function Walls() { 54 | return ( 55 | <> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 | export default function House() { 70 | 71 | return ( 72 | <> 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ) 84 | } 85 | 86 | function Roof() { 87 | return ( 88 | <> 89 | 90 | 91 | 92 | {[...Array(7)].map((x, i) => 93 | 94 | )} 95 | {[...Array(9)].map((x, i) => 96 | 97 | )} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {[...Array(7)].map((x, i) => 108 | 109 | )} 110 | {[...Array(9)].map((x, i) => 111 | 112 | )} 113 | 114 | 115 | 116 | 117 | 118 | {[...Array(9)].map((x, i) => 119 | 120 | )} 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ) 131 | } 132 | 133 | 134 | function EastRoofRow({x, y, z, material, double = false}) { 135 | return ( 136 | 137 | 138 | 139 | 140 | {double === true ? : null} 141 | 142 | 143 | {double === true ? : null} 144 | 145 | ) 146 | } 147 | 148 | function WestRoofRow({x, y, z, material, double = false}) { 149 | return ( 150 | 151 | 152 | 153 | 154 | {double === true ? 155 | : null} 156 | 157 | 158 | {double === true ? 159 | : null} 160 | 161 | ) 162 | } 163 | 164 | function CornerFlowerBed({x, y, z, facing}) { 165 | return ( 166 | 167 | 168 | 169 | 170 | 171 | 172 | ) 173 | } 174 | 175 | function LongFlowerBed({x, y, z, facing = 'west'}) { 176 | return ( 177 | 178 | {[...Array(5)].map((x, i) => 179 | <> 180 | 181 | 0.5 ? 'cornflower' : 'dandelion'} x={0} y={1} z={i}> 182 | 184 | 185 | )} 186 | 187 | 188 | ) 189 | } 190 | 191 | 192 | function Floor() { 193 | return ( 194 | <> 195 | {[...Array(10)].map((x, i) => 196 | [...Array(10)].map((y, j) => 197 | 1 && i < 8 && j > 1 && j < 8 ? 'spruce_planks' : 'grass_block'} snowy={false}> 198 | ) 199 | ) 200 | } 201 | 202 | ) 203 | 204 | } --------------------------------------------------------------------------------