new cool things (#58)
This commit is contained in:
commit
ceaf169c7b
40 changed files with 601 additions and 166 deletions
|
|
@ -1,5 +1,7 @@
|
|||
# Minecraft Web Client
|
||||
|
||||

|
||||
|
||||
A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using modern web technologies.
|
||||
|
||||
This project is a work in progress, but I consider it to be usable. If you encounter any bugs or usability issues, please report them!
|
||||
|
|
|
|||
BIN
assets/loading-bg.jpg
Normal file
BIN
assets/loading-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
docs-assets/banner.jpg
Normal file
BIN
docs-assets/banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
65
index.html
65
index.html
|
|
@ -4,22 +4,59 @@
|
|||
<script>
|
||||
window.startLoad = Date.now()
|
||||
</script>
|
||||
<script type="module">
|
||||
const checkLoadEruda = () => {
|
||||
if (window.location.hash === '#dev') {
|
||||
// todo precache (check offline)?
|
||||
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
||||
eruda.init()
|
||||
})
|
||||
<script async>
|
||||
const loadingDiv = `
|
||||
<div class="initial-loader" style="position: fixed;transition:opacity 0.2s;inset: 0;background:black;display: flex;justify-content: center;align-items: center;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif;gap: 15px;" ontransitionend="this.remove()">
|
||||
<div>
|
||||
<img src="./loading-bg.jpg" alt="Prismarine Web Client" style="position:fixed;inset:0;width:100%;height:100%;z-index: -2;object-fit: cover;filter: blur(3px);">
|
||||
<div style="position: fixed;inset: 0;z-index: -1;background-color: rgba(0, 0, 0, 0.8);"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;">Loading...</div>
|
||||
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);">A true Minecraft client in your browser!</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const loadingDivElem = document.createElement('div')
|
||||
loadingDivElem.innerHTML = loadingDiv
|
||||
if (!window.pageLoaded) {
|
||||
document.documentElement.appendChild(loadingDivElem)
|
||||
}
|
||||
}
|
||||
checkLoadEruda()
|
||||
window.addEventListener('hashchange', (e) => {
|
||||
setTimeout(() => {
|
||||
checkLoadEruda()
|
||||
</script>
|
||||
<script type="module" async>
|
||||
const checkLoadEruda = () => {
|
||||
if (window.location.hash === '#dev') {
|
||||
// todo precache (check offline)?
|
||||
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
||||
eruda.init()
|
||||
})
|
||||
}
|
||||
}
|
||||
checkLoadEruda()
|
||||
window.addEventListener('hashchange', (e) => {
|
||||
setTimeout(() => {
|
||||
checkLoadEruda()
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
<style>
|
||||
html {
|
||||
background: black;
|
||||
}
|
||||
.initial-loader {
|
||||
--font-size: 20px;
|
||||
}
|
||||
@media screen and (min-width: 550px) {
|
||||
.initial-loader {
|
||||
--font-size: 30px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 950px) {
|
||||
.initial-loader {
|
||||
--font-size: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>Prismarine Web Client</title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@dimaka/interface": "0.0.3-alpha.0",
|
||||
"@floating-ui/react": "^0.26.1",
|
||||
"@mui/base": "5.0.0-beta.34",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/wicg-file-system-access": "^2023.10.2",
|
||||
|
|
@ -45,14 +46,14 @@
|
|||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"express": "^4.18.2",
|
||||
"flying-squid": "github:zardoy/space-squid#everything",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.9",
|
||||
"fs-extra": "^11.1.1",
|
||||
"iconify-icon": "^1.0.8",
|
||||
"jszip": "^3.10.1",
|
||||
"lit": "^2.8.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minecraft-assets": "^1.12.2",
|
||||
"minecraft-data": "3.58.0",
|
||||
"minecraft-data": "3.60.0",
|
||||
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
|
||||
"node-gzip": "^1.1.2",
|
||||
"peerjs": "^1.5.0",
|
||||
|
|
@ -126,9 +127,9 @@
|
|||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.58.0",
|
||||
"minecraft-data": "3.60.0",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra",
|
||||
"minecraft-protocol": "github:zardoy/minecraft-protocol#everything",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
379
pnpm-lock.yaml
generated
379
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -28,7 +28,6 @@
|
|||
"looks-same": "^8.2.3",
|
||||
"minecraft-wrap": "^1.3.0",
|
||||
"minecrafthawkeye": "^1.3.6",
|
||||
"node-canvas-webgl": "^0.3.0",
|
||||
"prismarine-block": "^1.7.3",
|
||||
"prismarine-chunk": "^1.22.0",
|
||||
"prismarine-schematic": "^1.2.0",
|
||||
|
|
@ -40,5 +39,8 @@
|
|||
"three.meshline": "^1.3.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vec3": "^0.1.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-canvas-webgl": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export class Entities extends EventEmitter {
|
|||
this.debugMode = 'none'
|
||||
this.onSkinUpdate = () => { }
|
||||
this.clock = new THREE.Clock()
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
clear () {
|
||||
|
|
@ -102,6 +103,13 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
setVisible(visible, /** @type {THREE.Object3D?} */entity = null) {
|
||||
this.visible = visible
|
||||
for (const mesh of entity ? [entity] : Object.values(this.entities)) {
|
||||
mesh.visible = visible
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const dt = this.clock.getDelta()
|
||||
for (const entityId of Object.keys(this.entities)) {
|
||||
|
|
@ -284,6 +292,7 @@ export class Entities extends EventEmitter {
|
|||
this.updatePlayerSkin(entity.id, '', stevePng)
|
||||
}
|
||||
this.setDebugMode(this.debugMode, group)
|
||||
this.setVisible(this.visible, group)
|
||||
}
|
||||
|
||||
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class World {
|
|||
|
||||
const block = this.blockCache[stateId]
|
||||
block.position = loc
|
||||
block.biome = this.biomeCache[column.getBiome(locInChunk)]
|
||||
block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0]
|
||||
if (block.name === 'redstone_ore') block.transparent = false
|
||||
return block
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
// todo refactor into its own commons module
|
||||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/src/utils'
|
||||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { EventEmitter } from 'events'
|
||||
import { BotEvents } from 'mineflayer'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class WorldRenderer {
|
|||
showChunkBorders = false
|
||||
active = false
|
||||
version = undefined as string | undefined
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
loadedChunks = {}
|
||||
sectionsOutstanding = new Set()
|
||||
renderUpdateEmitter = new EventEmitter()
|
||||
|
|
@ -106,7 +107,9 @@ export class WorldRenderer {
|
|||
const [x, y, z] = posKey.split(',')
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
object.add(this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity)))
|
||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity));
|
||||
if (!sign) continue
|
||||
object.add(sign)
|
||||
}
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
|
|
@ -142,13 +145,43 @@ export class WorldRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
signsCache = new Map<string, any>()
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
textures = {}
|
||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||
}
|
||||
const texturekey = `${position.x},${position.y},${position.z}`;
|
||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
textures[texturekey] = tex
|
||||
return tex
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
// todo implement
|
||||
// const key = JSON.stringify({ position, rotation, isWall })
|
||||
// if (this.signsCache.has(key)) {
|
||||
// console.log('cached', key)
|
||||
// } else {
|
||||
// this.signsCache.set(key, tex)
|
||||
// }
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
|
|
@ -266,7 +299,17 @@ export class WorldRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
cleanChunkTextures (x, z) {
|
||||
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
|
||||
for (const key of Object.keys(textures)) {
|
||||
textures[key].dispose()
|
||||
delete textures[key]
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
this.cleanChunkTextures(x, z)
|
||||
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
|
|
@ -297,6 +340,7 @@ export class WorldRenderer {
|
|||
}
|
||||
|
||||
setSectionDirty (pos, value = true) {
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
// Dispatch sections to workers based on position
|
||||
// This guarantees uniformity accross workers and that a given section
|
||||
// is always dispatched to the same worker
|
||||
|
|
|
|||
|
|
@ -33,20 +33,28 @@ const parseSafe = (text: string, task: string) => {
|
|||
}
|
||||
|
||||
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
const factor = 50
|
||||
// todo don't use texture rendering, investigate the font rendering when possible
|
||||
// or increase factor when needed
|
||||
const factor = 40
|
||||
const signboardY = [16, 9]
|
||||
const heightOffset = signboardY[0] - signboardY[1]
|
||||
const heightScalar = heightOffset / 16
|
||||
|
||||
canvas.width = 16 * factor
|
||||
canvas.height = heightOffset * factor
|
||||
let canvas: HTMLCanvasElement | undefined
|
||||
let _ctx: CanvasRenderingContext2D | null = null
|
||||
const getCtx = () => {
|
||||
if (_ctx) return _ctx
|
||||
canvas = document.createElement('canvas')
|
||||
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
canvas.width = 16 * factor
|
||||
canvas.height = heightOffset * factor
|
||||
|
||||
ctxHook(ctx)
|
||||
_ctx = canvas.getContext('2d')!
|
||||
_ctx.imageSmoothingEnabled = false
|
||||
|
||||
ctxHook(_ctx)
|
||||
return _ctx
|
||||
}
|
||||
|
||||
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
||||
blockEntity.Text1,
|
||||
|
|
@ -104,8 +112,13 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
|||
if (stop) return false
|
||||
}
|
||||
}
|
||||
|
||||
renderText(rendered)
|
||||
|
||||
// skip rendering empty lines (and possible signs)
|
||||
if (!plainText.trim()) continue
|
||||
|
||||
const ctx = getCtx()
|
||||
const fontSize = 1.6 * factor;
|
||||
ctx.font = `${fontSize}px mojangles`
|
||||
const textWidth = ctx.measureText(plainText).width
|
||||
|
|
@ -115,7 +128,7 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
|||
// todo strikeStyle, underlineStyle
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
||||
ctx.fillText(text, (canvas.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
|
||||
ctx.fillText(text, (canvas!.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
|
||||
renderedWidth += ctx.measureText(text).width // todo isn't the font is monospace?
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ const blockEntity = {
|
|||
"Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}"
|
||||
} as const
|
||||
|
||||
await document.fonts.load('1em mojangles')
|
||||
|
||||
const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => {
|
||||
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||
})
|
||||
|
||||
document.body.appendChild(canvas)
|
||||
if (canvas) {
|
||||
canvas.style.imageRendering = 'pixelated'
|
||||
document.body.appendChild(canvas)
|
||||
} else {
|
||||
console.log('Render skipped')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ global.document = {
|
|||
const render = (entity) => {
|
||||
ctxTexts = []
|
||||
renderSign(entity, PrismarineChat)
|
||||
return ctxTexts.map(({ text, y }) => [y / 80, text])
|
||||
return ctxTexts.map(({ text, y }) => [y / 64, text])
|
||||
}
|
||||
|
||||
test('sign renderer', () => {
|
||||
|
|
@ -49,18 +49,6 @@ test('sign renderer', () => {
|
|||
1,
|
||||
"Tools",
|
||||
],
|
||||
[
|
||||
2,
|
||||
"",
|
||||
],
|
||||
[
|
||||
3,
|
||||
"",
|
||||
],
|
||||
[
|
||||
4,
|
||||
"",
|
||||
],
|
||||
]
|
||||
`)
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
["1.8.8", "1.9.4", "1.10.2", "1.11.2", "1.12.2", "1.13.2", "1.14.4", "1.15.2", "1.16.1", "1.16.4", "1.17.1", "1.18.1"]
|
||||
["1.8.8", "1.9.4", "1.10.2", "1.11.2", "1.12.2", "1.13.2", "1.14.4", "1.15.2", "1.16.1", "1.16.4", "1.17.1", "1.18.1", "1.18.2"]
|
||||
|
|
@ -4,6 +4,10 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
|||
import { join, dirname, basename } from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { filesize } from 'filesize'
|
||||
import MCProtocol from 'minecraft-protocol'
|
||||
import MCData from 'minecraft-data'
|
||||
|
||||
const { supportedVersions } = MCProtocol
|
||||
|
||||
const prod = process.argv.includes('--prod')
|
||||
let connectedClients = []
|
||||
|
|
@ -23,15 +27,14 @@ const plugins = [
|
|||
build.onLoad({
|
||||
filter: /minecraft-data[\/\\]data.js$/,
|
||||
}, (args) => {
|
||||
const version = supportedVersions.at(-1);
|
||||
const data = MCData(version)
|
||||
const defaultVersionsObj = {
|
||||
// default protocol data, needed for auto-version
|
||||
"1.20.1": {
|
||||
version: {
|
||||
"minecraftVersion": "1.20.1",
|
||||
"version": 763,
|
||||
"majorVersion": "1.20"
|
||||
},
|
||||
protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')),
|
||||
[version]: {
|
||||
version: data.version,
|
||||
// protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')),
|
||||
protocol: data.protocol,
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ for (const version of [...supportedVersions].reverse()) {
|
|||
if (fs.existsSync(dataPath)) {
|
||||
console.log('using blockCollisionShapes of version', version)
|
||||
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
|
||||
data.version = version
|
||||
processData(data)
|
||||
fs.writeFileSync('./generated/latestBlockCollisionsShapes.json', JSON.stringify(data), 'utf8')
|
||||
break
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const server = isProd ?
|
|||
}
|
||||
if (netInterface) {
|
||||
const address = netInterface.ip4
|
||||
console.log(`You can access the server on http://localhost:8080 or http://${address}:${port}`)
|
||||
console.log(`You can access the server on http://localhost:${port} or http://${address}:${port}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { options } from './optionsStorage'
|
|||
import { openPlayerInventory } from './playerWindows'
|
||||
import { chatInputValueGlobal } from './react/ChatContainer'
|
||||
import { fsState } from './loadSave'
|
||||
import { showOptionsModal } from './react/SelectOption'
|
||||
import widgets from './react/widgets'
|
||||
|
||||
// doesnt seem to work for now
|
||||
const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
|
||||
|
|
@ -290,6 +292,17 @@ export const f3Keybinds = [
|
|||
viewer.world.updateShowChunksBorder(options.showChunkBorders)
|
||||
},
|
||||
mobileTitle: 'Toggle chunk borders',
|
||||
},
|
||||
{
|
||||
key: 'KeyT',
|
||||
async action () {
|
||||
// waypoints
|
||||
const widgetNames = widgets.map(widget => widget.name)
|
||||
const widget = await showOptionsModal('Open Widget', widgetNames)
|
||||
if (!widget) return
|
||||
showModal({ reactType: `widget-${widget}` })
|
||||
},
|
||||
mobileTitle: 'Open Widget'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { LocalServer } from './customServer'
|
||||
|
||||
const { createMCServer } = require('flying-squid/src')
|
||||
const { createMCServer } = require('flying-squid/dist')
|
||||
|
||||
export const startLocalServer = (serverOptions) => {
|
||||
const passOptions = { ...serverOptions, Server: LocalServer }
|
||||
const server: NonNullable<typeof localServer> = createMCServer(passOptions)
|
||||
//@ts-expect-error browser patch
|
||||
server.formatMessage = (message) => `[server] ${message}`
|
||||
server.options = passOptions
|
||||
//@ts-expect-error todo remove
|
||||
server.looseProtocolMode = true
|
||||
return server
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ export function nameToMcOfflineUUID (name) {
|
|||
}
|
||||
|
||||
export async function savePlayers () {
|
||||
//@ts-expect-error TODO
|
||||
await localServer!.savePlayersSingleplayer()
|
||||
}
|
||||
|
||||
// todo flying squid should expose save function instead
|
||||
export const saveServer = async () => {
|
||||
if (!localServer || fsState.isReadonly) return
|
||||
const worlds = [localServer.overworld] as Array<import('prismarine-world').world.World>
|
||||
// todo
|
||||
const worlds = [(localServer as any).overworld] as Array<import('prismarine-world').world.World>
|
||||
await Promise.all([savePlayers(), ...worlds.map(async world => world.saveNow())])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { adoptBlockOrItemNamesFromLatest } from 'flying-squid/src/blockRenames'
|
||||
import { adoptBlockOrItemNamesFromLatest } from 'flying-squid/dist/blockRenames'
|
||||
import collisionShapesInit from '../generated/latestBlockCollisionsShapes.json'
|
||||
import outputInteractionShapesJson from './interactionShapesGenerated.json'
|
||||
|
||||
// defining globally to be used in loaded data, not sure of better workaround
|
||||
window.globalGetCollisionShapes = (version) => {
|
||||
// todo use the same in resourcepack
|
||||
const renamedBlocks = adoptBlockOrItemNamesFromLatest('blocks', version, Object.keys(collisionShapesInit.blocks))
|
||||
const versionFrom = collisionShapesInit.version
|
||||
const renamedBlocks = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(collisionShapesInit.blocks), versionFrom, version)
|
||||
const collisionShapes = {
|
||||
...collisionShapesInit,
|
||||
blocks: Object.fromEntries(Object.entries(collisionShapesInit.blocks).map(([, shape], i) => [renamedBlocks[i], shape]))
|
||||
|
|
@ -16,11 +17,12 @@ window.globalGetCollisionShapes = (version) => {
|
|||
export default () => {
|
||||
customEvents.on('gameLoaded', () => {
|
||||
// todo also remap block states (e.g. redstone)!
|
||||
const renamedBlocksInteraction = adoptBlockOrItemNamesFromLatest('blocks', bot.version, Object.keys(outputInteractionShapesJson))
|
||||
const renamedBlocksInteraction = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version)
|
||||
const interactionShapes = {
|
||||
...outputInteractionShapesJson,
|
||||
...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape]))
|
||||
}
|
||||
interactionShapes[''] = interactionShapes['air']
|
||||
// todo make earlier
|
||||
window.interactionShapes = interactionShapes
|
||||
})
|
||||
|
|
|
|||
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
|
|
@ -6,7 +6,7 @@ declare const bot: Omit<import('mineflayer').Bot, 'world'> & { world: import('pr
|
|||
declare const __type_bot: typeof bot
|
||||
declare const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer
|
||||
declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined
|
||||
declare const localServer: import('flying-squid/dist/types').FullServer & { options } | undefined
|
||||
declare const localServer: import('flying-squid/dist/index').FullServer & { options } | undefined
|
||||
/** all currently loaded mc data */
|
||||
declare const mcData: Record<string, any>
|
||||
declare const loadedData: import('minecraft-data').IndexedData
|
||||
|
|
|
|||
37
src/index.ts
37
src/index.ts
|
|
@ -7,6 +7,7 @@ import './devtools'
|
|||
import './entities'
|
||||
import initCollisionShapes from './getCollisionShapes'
|
||||
import { onGameLoad } from './playerWindows'
|
||||
import { supportedVersions } from 'minecraft-protocol'
|
||||
|
||||
import './menus/components/button'
|
||||
import './menus/components/edit_box'
|
||||
|
|
@ -364,6 +365,16 @@ async function connect (connectOptions: {
|
|||
const serverOptions = _.defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
|
||||
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
|
||||
const downloadMcData = async (version: string) => {
|
||||
// todo expose cache
|
||||
const lastVersion = supportedVersions.at(-1)
|
||||
if (version === lastVersion) {
|
||||
// ignore cache hit
|
||||
versionsByMinecraftVersion.pc[lastVersion]!['dataVersion']!++
|
||||
}
|
||||
if (!document.fonts.check('1em mojangles')) {
|
||||
// todo instead re-render signs on load
|
||||
await document.fonts.load('1em mojangles').catch(() => { })
|
||||
}
|
||||
setLoadingScreenStatus(`Downloading data for ${version}`)
|
||||
await downloadSoundsIfNeeded()
|
||||
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
|
||||
|
|
@ -449,11 +460,6 @@ async function connect (connectOptions: {
|
|||
respawn: options.autoRespawn,
|
||||
maxCatchupTicks: 0,
|
||||
async versionSelectedHook (client) {
|
||||
// todo keep in sync with esbuild preload, expose cache ideally
|
||||
if (client.version === '1.20.1') {
|
||||
// ignore cache hit
|
||||
versionsByMinecraftVersion.pc['1.20.1']!['dataVersion']!++
|
||||
}
|
||||
await downloadMcData(client.version)
|
||||
setLoadingScreenStatus(initialLoadingText)
|
||||
}
|
||||
|
|
@ -518,7 +524,7 @@ async function connect (connectOptions: {
|
|||
destroyAll()
|
||||
})
|
||||
|
||||
let lastPacket
|
||||
let lastPacket = undefined as string | undefined
|
||||
const packetBeforePlay = (_, __, ___, fullBuffer) => {
|
||||
lastPacket = fullBuffer.toString()
|
||||
}
|
||||
|
|
@ -553,8 +559,10 @@ async function connect (connectOptions: {
|
|||
setLoadingScreenStatus('Loading world')
|
||||
})
|
||||
|
||||
const spawnEarlier = !singleplayer && !p2pMultiplayer
|
||||
// don't use spawn event, player can be dead
|
||||
bot.once('health', () => {
|
||||
bot.once(spawnEarlier ? 'forcedMove' : 'health', () => {
|
||||
errorAbortController.abort()
|
||||
const mcData = MinecraftData(bot.version)
|
||||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
|
|
@ -725,7 +733,6 @@ async function connect (connectOptions: {
|
|||
hud.style.display = 'block'
|
||||
})
|
||||
|
||||
errorAbortController.abort()
|
||||
if (appStatusState.isError) return
|
||||
setLoadingScreenStatus(undefined)
|
||||
void viewer.waitForChunksToRender().then(() => {
|
||||
|
|
@ -789,6 +796,13 @@ document.body.addEventListener('touchstart', (e) => {
|
|||
}, { passive: false })
|
||||
// #endregion
|
||||
|
||||
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
|
||||
console.warn('Failed to load optional app config.json', error)
|
||||
return {}
|
||||
}).then((config) => {
|
||||
miscUiState.appConfig = config
|
||||
})
|
||||
|
||||
downloadAndOpenFile().then((downloadAction) => {
|
||||
if (downloadAction) return
|
||||
|
||||
|
|
@ -814,3 +828,10 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
console.error(err)
|
||||
alert(`Failed to download file: ${err}`)
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const initialLoader = document.querySelector('.initial-loader') as HTMLElement | null
|
||||
if (initialLoader) {
|
||||
initialLoader.style.opacity = '0'
|
||||
window.pageLoaded = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4196,7 +4196,7 @@
|
|||
16
|
||||
]
|
||||
},
|
||||
"light": "LightBlock",
|
||||
"light": [],
|
||||
"sunflower": [
|
||||
0,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import fs from 'fs'
|
||||
import { supportedVersions } from 'flying-squid/src/lib/version'
|
||||
import { supportedVersions } from 'flying-squid/dist/lib/version'
|
||||
import * as nbt from 'prismarine-nbt'
|
||||
import { proxy } from 'valtio'
|
||||
import { gzip } from 'node-gzip'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//@ts-check
|
||||
const { LitElement, html, css } = require('lit')
|
||||
const viewerSupportedVersions = require('prismarine-viewer/viewer/supportedVersions.json')
|
||||
const { supportedVersions } = require('minecraft-protocol')
|
||||
const { hideCurrentModal, miscUiState } = require('../globalState')
|
||||
const { default: supportedVersions } = require('../supportedVersions.mjs')
|
||||
const { commonCss } = require('./components/common')
|
||||
|
||||
const fullySupporedVersions = viewerSupportedVersions
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const defaultOptions = {
|
|||
askGuestName: true,
|
||||
/** Actually might be useful */
|
||||
showCursorBlockInSpectator: false,
|
||||
renderEntities: true,
|
||||
|
||||
// advanced bot options
|
||||
autoRespawn: false,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Button from './Button'
|
|||
import styles from './createWorld.module.css'
|
||||
|
||||
// const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states']
|
||||
const worldTypes = ['plains', 'flat'/* , 'void' */]
|
||||
const worldTypes = ['default', 'flat'/* , 'void' */]
|
||||
|
||||
export const creatingWorldState = proxy({
|
||||
title: '',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { supportedVersions } from 'flying-squid/src/lib/version'
|
||||
import { supportedVersions } from 'flying-squid/dist/lib/version'
|
||||
import { hideCurrentModal, showModal } from '../globalState'
|
||||
import defaultLocalServerOptions from '../defaultLocalServerOptions'
|
||||
import { mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
|
||||
|
|
|
|||
10
src/react/FullScreenWidget.tsx
Normal file
10
src/react/FullScreenWidget.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Screen from './Screen'
|
||||
import { useIsWidgetActive } from './utils'
|
||||
|
||||
export default ({ name, title, children }) => {
|
||||
const isWidgetActive = useIsWidgetActive(name)
|
||||
|
||||
if (!isWidgetActive) return null
|
||||
|
||||
return <Screen backdrop title={title}>{children}</Screen>
|
||||
}
|
||||
45
src/react/ModuleSignsViewer.tsx
Normal file
45
src/react/ModuleSignsViewer.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import FullScreenWidget from './FullScreenWidget'
|
||||
|
||||
export const name = 'signs'
|
||||
|
||||
export default () => {
|
||||
const signs = [...viewer.world.chunkTextures.values()].flatMap(textures => {
|
||||
return Object.entries(textures).map(([signPosKey, texture]) => {
|
||||
const pos = signPosKey.split(',').map(Number)
|
||||
return <div key={signPosKey}>
|
||||
<div style={{ color: 'white' }}>{pos.join(', ')}</div>
|
||||
<div style={{ background: 'rgba(255, 255, 255, 0.5)', padding: 5, borderRadius: 5 }}>
|
||||
<AddElem elem={texture.image} />
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
})
|
||||
|
||||
return <FullScreenWidget name='signs' title='Loaded Signs'>
|
||||
<div>
|
||||
{signs.length} signs currently loaded:
|
||||
</div>
|
||||
{signs}
|
||||
</FullScreenWidget>
|
||||
}
|
||||
|
||||
const AddElem = ({ elem }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
elem.style.width = '100%'
|
||||
ref.current!.appendChild(elem)
|
||||
return () => {
|
||||
elem.remove()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div ref={ref}></div>
|
||||
}
|
||||
|
||||
// for (const key of Object.keys(viewer.world.sectionObjects)) {
|
||||
// const section = viewer.world.sectionObjects[key]
|
||||
// for (const child of section.children) {
|
||||
// if (child.name === 'mesh') child.visible = false
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
.potential-problem {
|
||||
color: #808080;
|
||||
word-break: break-all;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.last-status {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export const useIsModalActive = (modal: string) => {
|
|||
return useSnapshot(activeModalStack).at(-1)?.reactType === modal
|
||||
}
|
||||
|
||||
export const useIsWidgetActive = (name: string) => {
|
||||
return useIsModalActive(`widget-${name}`)
|
||||
}
|
||||
|
||||
export function useDidUpdateEffect (fn, inputs) {
|
||||
const isMountingRef = useRef(false)
|
||||
|
||||
|
|
|
|||
3
src/react/widgets.ts
Normal file
3
src/react/widgets.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import * as ModuleSignsViewer from './ModuleSignsViewer'
|
||||
|
||||
export default [ModuleSignsViewer]
|
||||
|
|
@ -16,6 +16,8 @@ import EnterFullscreenButton from './react/EnterFullscreenButton'
|
|||
import ChatProvider from './react/ChatProvider'
|
||||
import SoundMuffler from './react/SoundMuffler'
|
||||
import TouchControls from './react/TouchControls'
|
||||
import widgets from './react/widgets'
|
||||
import { useIsWidgetActive } from './react/utils'
|
||||
|
||||
const Portal = ({ children, to }) => {
|
||||
return createPortal(children, to)
|
||||
|
|
@ -65,11 +67,23 @@ const InGameUi = () => {
|
|||
</>
|
||||
}
|
||||
|
||||
const AllWidgets = () => {
|
||||
return widgets.map(widget => <WidgetDisplay key={widget.name} name={widget.name} Component={widget.default} />)
|
||||
}
|
||||
|
||||
const WidgetDisplay = ({ name, Component }) => {
|
||||
const isWidgetActive = useIsWidgetActive(name)
|
||||
if (!isWidgetActive) return null
|
||||
|
||||
return <Component />
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
return <div>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<Portal to={document.querySelector('#ui-root')}>
|
||||
<AllWidgets />
|
||||
<SingleplayerProvider />
|
||||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
|
||||
let lastStepSound = 0
|
||||
const movementHappening = async () => {
|
||||
if (!bot.player) return // no info yet
|
||||
const VELOCITY_THRESHOLD = 0.1
|
||||
const { x, z, y } = bot.player.entity.velocity
|
||||
if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
|
|||
export const disconnect = async () => {
|
||||
if (localServer) {
|
||||
await saveServer()
|
||||
//@ts-expect-error todo expose!
|
||||
void localServer.quit() // todo investigate we should await
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}`) // remove qs
|
||||
|
|
|
|||
|
|
@ -29,4 +29,8 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
viewer.composer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.entities.setVisible(o.renderEntities)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,8 +189,14 @@ class WorldInteraction {
|
|||
//@ts-expect-error todo
|
||||
bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn)
|
||||
} else {
|
||||
// https://discord.com/channels/413438066984747026/413438150594265099/1198724637572477098
|
||||
const oldLookAt = bot.lookAt
|
||||
//@ts-expect-error
|
||||
bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).catch(console.warn)
|
||||
bot.lookAt = (pos) => { }
|
||||
//@ts-expect-error
|
||||
bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).finally(() => {
|
||||
bot.lookAt = oldLookAt
|
||||
}).catch(console.warn)
|
||||
}
|
||||
this.lastBlockPlaced = 0
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue