├── 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 | [](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 |
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 |
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 | }
--------------------------------------------------------------------------------