Merge remote-tracking branch 'origin/develop' into webgpu

This commit is contained in:
Vitaly Turovsky 2024-02-27 03:22:01 +03:00
commit ac07b5cfbc
68 changed files with 4118 additions and 530 deletions

View file

@ -27,6 +27,7 @@ jobs:
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- run: pnpm build-storybook
- name: Copy playground files
run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
- name: Download Generated Sounds map

View file

@ -8,6 +8,9 @@ After forking the repository, run the following commands to get started:
A few notes:
- Use `next` branch for development and as base & target branch for pull requests if possible.
- To link dependency locally e.g. flying-squid add this to `pnpm` > `overrides` of root package.json: `"flying-squid": "file:../space-squid",` (with some modules `pnpm link` also works)
- It's recommended to use debugger for debugging. VSCode has a great debugger built-in. If debugger is slow, you can use `--no-sources` flag that would allow browser to speedup .map file parsing.
- Some data are cached between restarts. If you see something doesn't work after upgrading dependencies, try to clear the by simply removing the `dist` folder.
- The same folder `dist` is used for both development and production builds, so be careful when deploying the project.

View file

@ -6,7 +6,7 @@ A true Minecraft client running in your browser! A port of the original game to
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!
You can try this out at [mcraft.fun](https://mcraft.fun/), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy.
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link) [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the `develop` (default) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) - so it's usually newer, but might be less stable.
### Big Features
@ -83,6 +83,20 @@ You can also drag and drop any .dat or .mca (region files) into the browser wind
world chunks have a *yellow* border, hostile mobs have a *red* outline, passive mobs have a *green* outline, players have a *blue* outline.
### Query Parameters
Press `Y` to set query parameters to url of your current game state.
- `?server=<server_address>` - Display connect screen to the server on load
- `?username=<username>` - Set the username on load
- `?proxy=<proxy_address>` - Set the proxy server address on load
- `?version=<version>` - Set the version on load
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
<!-- - `?password=<password>` - Set the password on load -->
- `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title)
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
- `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work
### Notable Things that Power this Project
- [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked

BIN
assets/generic_91.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/generic_92.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/generic_93.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/generic_94.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/generic_95.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -3,5 +3,5 @@
"defaultHost": "<from-proxy>",
"defaultProxy": "zardoy.site:2344",
"defaultVersion": "1.18.2",
"mapsProvider": "zardoy.site/maps"
"mapsProvider": "https://maps.mcraft.fun/"
}

View file

@ -71,7 +71,8 @@ const buildOptions = {
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`)
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`),
'process.env.DEPS_VERSIONS': JSON.stringify({})
},
loader: {
// todo use external or resolve issues with duplicating

View file

@ -4,41 +4,53 @@
<script>
window.startLoad = Date.now()
</script>
<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)
}
</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()
})
<!-- // #region initial loader -->
<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;" class="title">Loading...</div>
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);" class="subtitle">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)
}
// load error handling
const onError = (message) => {
console.log(message)
if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') {
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = message
window.location.hash = '#dev' // show eruda
}
}
checkLoadEruda()
window.addEventListener('hashchange', (e) => {
setTimeout(() => {
checkLoadEruda()
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
window.addEventListener('error', (e) => onError(e.message))
</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;
@ -57,6 +69,7 @@
}
}
</style>
<!-- // #endregion -->
<title>Prismarine Web Client</title>
<link rel="stylesheet" href="index.css">
<link rel="favicon" href="favicon.ico">

View file

@ -30,6 +30,7 @@
"@dimaka/interface": "0.0.3-alpha.0",
"@floating-ui/react": "^0.26.1",
"@mui/base": "5.0.0-beta.34",
"@nxg-org/mineflayer-tracker": "^1.2.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/wicg-file-system-access": "^2023.10.2",
@ -53,7 +54,7 @@
"lit": "^2.8.0",
"lodash-es": "^4.17.21",
"minecraft-assets": "^1.12.2",
"minecraft-data": "3.60.0",
"minecraft-data": "3.61.2",
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
"node-gzip": "^1.1.2",
"peerjs": "^1.5.0",
@ -64,10 +65,12 @@
"react-transition-group": "^4.4.5",
"sanitize-filename": "^1.6.3",
"skinview3d": "^3.0.1",
"source-map-js": "^1.0.2",
"stats-gl": "^1.0.5",
"stats.js": "^0.17.0",
"tabbable": "^6.2.0",
"title-case": "3.x",
"ua-parser-js": "^1.0.37",
"valtio": "^1.11.1",
"workbox-build": "^7.0.0"
},
@ -127,7 +130,7 @@
"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.60.0",
"minecraft-data": "3.61.2",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"minecraft-protocol": "github:zardoy/minecraft-protocol#everything",
"react": "^18.2.0"

596
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,8 @@ import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToC
import stevePng from 'minecraft-assets/minecraft-assets/data/1.20.2/entity/player/wide/steve.png'
import { WalkingGeneralSwing } from './entity/animations'
import { NameTagObject } from 'skinview3d/libs/nametag'
import { fromFormattedString } from '@xmcl/text-component'
import { flat, fromFormattedString } from '@xmcl/text-component'
import mojangson from 'mojangson'
export const TWEEN_DURATION = 50 // todo should be 100
@ -39,6 +40,23 @@ function getUsernameTexture (username, { fontFamily = 'sans-serif' }) {
return canvas
}
const addNametag = (entity, options, mesh) => {
if (entity.username !== undefined) {
if (mesh.children.find(c => c.name === 'nametag')) return // todo update
const canvas = getUsernameTexture(entity.username, options)
const tex = new THREE.Texture(canvas)
tex.needsUpdate = true
const spriteMat = new THREE.SpriteMaterial({ map: tex })
const sprite = new THREE.Sprite(spriteMat)
sprite.renderOrder = 1000
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
sprite.position.y += entity.height + 0.6
sprite.name = 'nametag'
mesh.add(sprite)
}
}
function getEntityMesh (entity, scene, options, overrides) {
if (entity.name) {
try {
@ -46,18 +64,7 @@ function getEntityMesh (entity, scene, options, overrides) {
const entityName = entity.name.toLowerCase()
const e = new Entity('1.16.4', entityName, scene, overrides)
if (entity.username !== undefined) {
const canvas = getUsernameTexture(entity.username, options)
const tex = new THREE.Texture(canvas)
tex.needsUpdate = true
const spriteMat = new THREE.SpriteMaterial({ map: tex })
const sprite = new THREE.Sprite(spriteMat)
sprite.renderOrder = 1000
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
sprite.position.y += entity.height + 0.6
e.mesh.add(sprite)
}
addNametag(entity, options, e.mesh)
return e.mesh
} catch (err) {
console.log(err)
@ -126,6 +133,9 @@ export class Entities extends EventEmitter {
return playerObject
}
// fixme workaround
defaultSteveTexture
// true means use default skin url
updatePlayerSkin (entityId, username, /** @type {string | true} */skinUrl, /** @type {string | true | undefined} */capeUrl = undefined) {
let playerObject = this.getPlayerObject(entityId)
@ -139,15 +149,24 @@ export class Entities extends EventEmitter {
loadImage(skinUrl).then((image) => {
playerObject = this.getPlayerObject(entityId)
if (!playerObject) return
const skinCanvas = document.createElement('canvas')
loadSkinToCanvas(skinCanvas, image)
const skinTexture = new THREE.CanvasTexture(skinCanvas)
/** @type {THREE.CanvasTexture} */
let skinTexture
if (skinUrl === stevePng && this.defaultSteveTexture) {
skinTexture = this.defaultSteveTexture
} else {
const skinCanvas = document.createElement('canvas')
loadSkinToCanvas(skinCanvas, image)
skinTexture = new THREE.CanvasTexture(skinCanvas)
if (skinUrl === stevePng) {
this.defaultSteveTexture = skinTexture
}
}
skinTexture.magFilter = THREE.NearestFilter
skinTexture.minFilter = THREE.NearestFilter
skinTexture.needsUpdate = true
//@ts-ignore
playerObject.skin.map = skinTexture
playerObject.skin.modelType = inferModelType(skinCanvas)
playerObject.skin.modelType = inferModelType(skinTexture.image)
const earsCanvas = document.createElement('canvas')
loadEarsToCanvasFromSkin(earsCanvas, image)
@ -186,9 +205,9 @@ export class Entities extends EventEmitter {
if (!playerObject.backEquipment) {
playerObject.backEquipment = 'cape'
}
})
}, () => {})
}
})
}, () => {})
playerObject.cape.visible = false
@ -228,6 +247,13 @@ export class Entities extends EventEmitter {
}
displaySimpleText (jsonLike) {
if (!jsonLike) return
const parsed = mojangson.simplify(mojangson.parse(jsonLike))
const text = flat(parsed).map(x => x.text)
return text.join('')
}
update (/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
if (!this.entities[entity.id] && !entity.delete) {
const group = new THREE.Group()
@ -295,6 +321,48 @@ export class Entities extends EventEmitter {
this.setVisible(this.visible, group)
}
//@ts-ignore
const isInvisible = entity.metadata?.[0] & 0x20
if (isInvisible) {
for (const child of this.entities[entity.id].children.find(c => c.name === 'mesh').children) {
if (child.name !== 'nametag') {
child.visible = false
}
}
}
// not player
const displayText = entity.metadata?.[3] && this.displaySimpleText(entity.metadata[2]);
if (entity.name !== 'player' && displayText) {
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
}
// todo handle map, map_chunks events
// if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') {
// const example = {
// "present": true,
// "itemId": 847,
// "itemCount": 1,
// "nbtData": {
// "type": "compound",
// "name": "",
// "value": {
// "map": {
// "type": "int",
// "value": 2146483444
// },
// "interactiveboard": {
// "type": "byte",
// "value": 1
// }
// }
// }
// }
// const item = entity.metadata?.[8]
// if (item.nbtData) {
// const nbt = nbt.simplify(item.nbtData)
// }
// }
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
const e = this.entities[entity.id]

View file

@ -179,5 +179,536 @@
"flowering_azalea": true,
"frogspawn": true,
"decorated_pot": true
},
"colors": {
"stone": "rgb(112, 112, 112)",
"granite": "rgb(151, 109, 77)",
"polished_granite": "rgb(151, 109, 77)",
"diorite": "rgb(255, 252, 245)",
"polished_diorite": "rgb(255, 252, 245)",
"andesite": "rgb(112, 112, 112)",
"polished_andesite": "rgb(112, 112, 112)",
"grass_block": "rgb(127, 178, 56)",
"dirt": "rgb(151, 109, 77)",
"coarse_dirt": "rgb(151, 109, 77)",
"podzol": "rgb(129, 86, 49)",
"cobblestone": "rgb(112, 112, 112)",
"oak_planks": "rgb(143, 119, 72)",
"spruce_planks": "rgb(129, 86, 49)",
"birch_planks": "rgb(247, 233, 163)",
"jungle_planks": "rgb(151, 109, 77)",
"acacia_planks": "rgb(216, 127, 51)",
"cherry_planks": "rgb(209, 177, 161)",
"dark_oak_planks": "rgb(102, 76, 51)",
"mangrove_planks": "rgb(153, 51, 51)",
"bamboo_planks": "rgb(229, 229, 51)",
"bamboo_mosaic": "rgb(229, 229, 51)",
"oak_sapling": "rgb(0, 124, 0)",
"spruce_sapling": "rgb(0, 124, 0)",
"birch_sapling": "rgb(0, 124, 0)",
"jungle_sapling": "rgb(0, 124, 0)",
"acacia_sapling": "rgb(0, 124, 0)",
"cherry_sapling": "rgb(242, 127, 165)",
"dark_oak_sapling": "rgb(0, 124, 0)",
"mangrove_propagule": "rgb(0, 124, 0)",
"bedrock": "rgb(112, 112, 112)",
"water": "rgb(64, 64, 255)",
"lava": "rgb(255, 0, 0)",
"sand": "rgb(247, 233, 163)",
"suspicious_sand": "rgb(247, 233, 163)",
"red_sand": "rgb(216, 127, 51)",
"gravel": "rgb(112, 112, 112)",
"suspicious_gravel": "rgb(112, 112, 112)",
"gold_ore": "rgb(112, 112, 112)",
"deepslate_gold_ore": "rgb(100, 100, 100)",
"iron_ore": "rgb(112, 112, 112)",
"deepslate_iron_ore": "rgb(100, 100, 100)",
"coal_ore": "rgb(112, 112, 112)",
"deepslate_coal_ore": "rgb(100, 100, 100)",
"nether_gold_ore": "rgb(112, 2, 0)",
"mangrove_roots": "rgb(129, 86, 49)",
"muddy_mangrove_roots": "rgb(129, 86, 49)",
"oak_wood": "rgb(143, 119, 72)",
"spruce_wood": "rgb(129, 86, 49)",
"birch_wood": "rgb(247, 233, 163)",
"jungle_wood": "rgb(151, 109, 77)",
"acacia_wood": "rgb(76, 76, 76)",
"cherry_wood": "rgb(57, 41, 35)",
"dark_oak_wood": "rgb(102, 76, 51)",
"mangrove_wood": "rgb(153, 51, 51)",
"stripped_oak_wood": "rgb(143, 119, 72)",
"stripped_spruce_wood": "rgb(129, 86, 49)",
"stripped_birch_wood": "rgb(247, 233, 163)",
"stripped_jungle_wood": "rgb(151, 109, 77)",
"stripped_acacia_wood": "rgb(216, 127, 51)",
"stripped_cherry_wood": "rgb(160, 77, 78)",
"stripped_dark_oak_wood": "rgb(102, 76, 51)",
"oak_leaves": "rgb(0, 124, 0)",
"spruce_leaves": "rgb(0, 124, 0)",
"birch_leaves": "rgb(0, 124, 0)",
"jungle_leaves": "rgb(0, 124, 0)",
"acacia_leaves": "rgb(0, 124, 0)",
"cherry_leaves": "rgb(242, 127, 165)",
"dark_oak_leaves": "rgb(0, 124, 0)",
"mangrove_leaves": "rgb(0, 124, 0)",
"azalea_leaves": "rgb(0, 124, 0)",
"flowering_azalea_leaves": "rgb(0, 124, 0)",
"sponge": "rgb(229, 229, 51)",
"wet_sponge": "rgb(229, 229, 51)",
"lapis_ore": "rgb(112, 112, 112)",
"deepslate_lapis_ore": "rgb(100, 100, 100)",
"lapis_block": "rgb(74, 128, 255)",
"dispenser": "rgb(112, 112, 112)",
"sandstone": "rgb(247, 233, 163)",
"chiseled_sandstone": "rgb(247, 233, 163)",
"cut_sandstone": "rgb(247, 233, 163)",
"note_block": "rgb(143, 119, 72)",
"sticky_piston": "rgb(112, 112, 112)",
"cobweb": "rgb(199, 199, 199)",
"grass": "rgb(0, 124, 0)",
"fern": "rgb(0, 124, 0)",
"dead_bush": "rgb(143, 119, 72)",
"seagrass": "rgb(64, 64, 255)",
"tall_seagrass": "rgb(64, 64, 255)",
"piston": "rgb(112, 112, 112)",
"piston_head": "rgb(112, 112, 112)",
"white_wool": "rgb(255, 255, 255)",
"orange_wool": "rgb(216, 127, 51)",
"magenta_wool": "rgb(178, 76, 216)",
"light_blue_wool": "rgb(102, 153, 216)",
"yellow_wool": "rgb(229, 229, 51)",
"lime_wool": "rgb(127, 204, 25)",
"pink_wool": "rgb(242, 127, 165)",
"gray_wool": "rgb(76, 76, 76)",
"light_gray_wool": "rgb(153, 153, 153)",
"cyan_wool": "rgb(76, 127, 153)",
"purple_wool": "rgb(127, 63, 178)",
"blue_wool": "rgb(51, 76, 178)",
"brown_wool": "rgb(102, 76, 51)",
"green_wool": "rgb(102, 127, 51)",
"red_wool": "rgb(153, 51, 51)",
"black_wool": "rgb(25, 25, 25)",
"moving_piston": "rgb(112, 112, 112)",
"dandelion": "rgb(0, 124, 0)",
"torchflower": "rgb(0, 124, 0)",
"poppy": "rgb(0, 124, 0)",
"blue_orchid": "rgb(0, 124, 0)",
"allium": "rgb(0, 124, 0)",
"azure_bluet": "rgb(0, 124, 0)",
"red_tulip": "rgb(0, 124, 0)",
"orange_tulip": "rgb(0, 124, 0)",
"white_tulip": "rgb(0, 124, 0)",
"pink_tulip": "rgb(0, 124, 0)",
"oxeye_daisy": "rgb(0, 124, 0)",
"cornflower": "rgb(0, 124, 0)",
"wither_rose": "rgb(0, 124, 0)",
"lily_of_the_valley": "rgb(0, 124, 0)",
"brown_mushroom": "rgb(102, 76, 51)",
"red_mushroom": "rgb(153, 51, 51)",
"gold_block": "rgb(250, 238, 77)",
"iron_block": "rgb(167, 167, 167)",
"bricks": "rgb(153, 51, 51)",
"tnt": "rgb(255, 0, 0)",
"bookshelf": "rgb(143, 119, 72)",
"chiseled_bookshelf": "rgb(143, 119, 72)",
"mossy_cobblestone": "rgb(112, 112, 112)",
"obsidian": "rgb(25, 25, 25)",
"fire": "rgb(255, 0, 0)",
"soul_fire": "rgb(102, 153, 216)",
"spawner": "rgb(112, 112, 112)",
"chest": "rgb(143, 119, 72)",
"diamond_ore": "rgb(112, 112, 112)",
"deepslate_diamond_ore": "rgb(100, 100, 100)",
"diamond_block": "rgb(92, 219, 213)",
"crafting_table": "rgb(143, 119, 72)",
"wheat": "rgb(0, 124, 0)",
"farmland": "rgb(151, 109, 77)",
"furnace": "rgb(112, 112, 112)",
"oak_sign": "rgb(143, 119, 72)",
"birch_sign": "rgb(247, 233, 163)",
"acacia_sign": "rgb(216, 127, 51)",
"oak_wall_sign": "rgb(143, 119, 72)",
"birch_wall_sign": "rgb(247, 233, 163)",
"acacia_wall_sign": "rgb(216, 127, 51)",
"birch_hanging_sign": "rgb(247, 233, 163)",
"acacia_hanging_sign": "rgb(216, 127, 51)",
"cherry_hanging_sign": "rgb(160, 77, 78)",
"crimson_hanging_sign": "rgb(148, 63, 97)",
"warped_hanging_sign": "rgb(58, 142, 140)",
"bamboo_hanging_sign": "rgb(229, 229, 51)",
"spruce_wall_hanging_sign": "rgb(143, 119, 72)",
"birch_wall_hanging_sign": "rgb(247, 233, 163)",
"acacia_wall_hanging_sign": "rgb(216, 127, 51)",
"cherry_wall_hanging_sign": "rgb(160, 77, 78)",
"crimson_wall_hanging_sign": "rgb(148, 63, 97)",
"warped_wall_hanging_sign": "rgb(58, 142, 140)",
"bamboo_wall_hanging_sign": "rgb(229, 229, 51)",
"stone_pressure_plate": "rgb(112, 112, 112)",
"iron_door": "rgb(167, 167, 167)",
"redstone_ore": "rgb(112, 112, 112)",
"deepslate_redstone_ore": "rgb(100, 100, 100)",
"snow": "rgb(255, 255, 255)",
"ice": "rgb(160, 160, 255)",
"snow_block": "rgb(255, 255, 255)",
"cactus": "rgb(0, 124, 0)",
"clay": "rgb(164, 168, 184)",
"sugar_cane": "rgb(0, 124, 0)",
"jukebox": "rgb(151, 109, 77)",
"pumpkin": "rgb(216, 127, 51)",
"netherrack": "rgb(112, 2, 0)",
"soul_sand": "rgb(102, 76, 51)",
"soul_soil": "rgb(102, 76, 51)",
"basalt": "rgb(25, 25, 25)",
"polished_basalt": "rgb(25, 25, 25)",
"glowstone": "rgb(247, 233, 163)",
"carved_pumpkin": "rgb(216, 127, 51)",
"jack_o_lantern": "rgb(216, 127, 51)",
"oak_trapdoor": "rgb(143, 119, 72)",
"spruce_trapdoor": "rgb(129, 86, 49)",
"birch_trapdoor": "rgb(247, 233, 163)",
"jungle_trapdoor": "rgb(151, 109, 77)",
"acacia_trapdoor": "rgb(216, 127, 51)",
"cherry_trapdoor": "rgb(209, 177, 161)",
"dark_oak_trapdoor": "rgb(102, 76, 51)",
"mangrove_trapdoor": "rgb(153, 51, 51)",
"bamboo_trapdoor": "rgb(229, 229, 51)",
"stone_bricks": "rgb(112, 112, 112)",
"mossy_stone_bricks": "rgb(112, 112, 112)",
"cracked_stone_bricks": "rgb(112, 112, 112)",
"chiseled_stone_bricks": "rgb(112, 112, 112)",
"mud_bricks": "rgb(135, 107, 98)",
"infested_stone": "rgb(164, 168, 184)",
"infested_cobblestone": "rgb(164, 168, 184)",
"infested_stone_bricks": "rgb(164, 168, 184)",
"infested_mossy_stone_bricks": "rgb(164, 168, 184)",
"infested_cracked_stone_bricks": "rgb(164, 168, 184)",
"infested_chiseled_stone_bricks": "rgb(164, 168, 184)",
"brown_mushroom_block": "rgb(151, 109, 77)",
"red_mushroom_block": "rgb(153, 51, 51)",
"mushroom_stem": "rgb(199, 199, 199)",
"melon": "rgb(127, 204, 25)",
"attached_pumpkin_stem": "rgb(0, 124, 0)",
"attached_melon_stem": "rgb(0, 124, 0)",
"pumpkin_stem": "rgb(0, 124, 0)",
"melon_stem": "rgb(0, 124, 0)",
"vine": "rgb(0, 124, 0)",
"glow_lichen": "rgb(127, 167, 150)",
"mycelium": "rgb(127, 63, 178)",
"lily_pad": "rgb(0, 124, 0)",
"nether_bricks": "rgb(112, 2, 0)",
"nether_brick_fence": "rgb(112, 2, 0)",
"nether_wart": "rgb(153, 51, 51)",
"enchanting_table": "rgb(153, 51, 51)",
"brewing_stand": "rgb(167, 167, 167)",
"cauldron": "rgb(112, 112, 112)",
"end_portal": "rgb(25, 25, 25)",
"end_portal_frame": "rgb(102, 127, 51)",
"end_stone": "rgb(247, 233, 163)",
"dragon_egg": "rgb(25, 25, 25)",
"cocoa": "rgb(0, 124, 0)",
"emerald_ore": "rgb(112, 112, 112)",
"deepslate_emerald_ore": "rgb(100, 100, 100)",
"ender_chest": "rgb(112, 112, 112)",
"emerald_block": "rgb(0, 217, 58)",
"command_block": "rgb(102, 76, 51)",
"beacon": "rgb(92, 219, 213)",
"carrots": "rgb(0, 124, 0)",
"potatoes": "rgb(0, 124, 0)",
"anvil": "rgb(167, 167, 167)",
"chipped_anvil": "rgb(167, 167, 167)",
"damaged_anvil": "rgb(167, 167, 167)",
"trapped_chest": "rgb(143, 119, 72)",
"light_weighted_pressure_plate": "rgb(250, 238, 77)",
"heavy_weighted_pressure_plate": "rgb(167, 167, 167)",
"daylight_detector": "rgb(143, 119, 72)",
"redstone_block": "rgb(255, 0, 0)",
"nether_quartz_ore": "rgb(112, 2, 0)",
"hopper": "rgb(112, 112, 112)",
"quartz_block": "rgb(255, 252, 245)",
"chiseled_quartz_block": "rgb(255, 252, 245)",
"quartz_pillar": "rgb(255, 252, 245)",
"dropper": "rgb(112, 112, 112)",
"white_terracotta": "rgb(209, 177, 161)",
"orange_terracotta": "rgb(159, 82, 36)",
"magenta_terracotta": "rgb(149, 87, 108)",
"light_blue_terracotta": "rgb(112, 108, 138)",
"yellow_terracotta": "rgb(186, 133, 36)",
"lime_terracotta": "rgb(103, 117, 53)",
"pink_terracotta": "rgb(160, 77, 78)",
"gray_terracotta": "rgb(57, 41, 35)",
"light_gray_terracotta": "rgb(135, 107, 98)",
"cyan_terracotta": "rgb(87, 92, 92)",
"purple_terracotta": "rgb(122, 73, 88)",
"blue_terracotta": "rgb(76, 62, 92)",
"brown_terracotta": "rgb(76, 50, 35)",
"green_terracotta": "rgb(76, 82, 42)",
"red_terracotta": "rgb(142, 60, 46)",
"black_terracotta": "rgb(37, 22, 16)",
"slime_block": "rgb(127, 178, 56)",
"iron_trapdoor": "rgb(167, 167, 167)",
"prismarine": "rgb(76, 127, 153)",
"prismarine_bricks": "rgb(92, 219, 213)",
"dark_prismarine": "rgb(92, 219, 213)",
"prismarine_slab": "rgb(76, 127, 153)",
"prismarine_brick_slab": "rgb(92, 219, 213)",
"dark_prismarine_slab": "rgb(92, 219, 213)",
"sea_lantern": "rgb(255, 252, 245)",
"hay_block": "rgb(229, 229, 51)",
"white_carpet": "rgb(255, 255, 255)",
"orange_carpet": "rgb(216, 127, 51)",
"magenta_carpet": "rgb(178, 76, 216)",
"light_blue_carpet": "rgb(102, 153, 216)",
"yellow_carpet": "rgb(229, 229, 51)",
"lime_carpet": "rgb(127, 204, 25)",
"pink_carpet": "rgb(242, 127, 165)",
"gray_carpet": "rgb(76, 76, 76)",
"light_gray_carpet": "rgb(153, 153, 153)",
"cyan_carpet": "rgb(76, 127, 153)",
"purple_carpet": "rgb(127, 63, 178)",
"blue_carpet": "rgb(51, 76, 178)",
"brown_carpet": "rgb(102, 76, 51)",
"green_carpet": "rgb(102, 127, 51)",
"red_carpet": "rgb(153, 51, 51)",
"black_carpet": "rgb(25, 25, 25)",
"terracotta": "rgb(216, 127, 51)",
"coal_block": "rgb(25, 25, 25)",
"packed_ice": "rgb(160, 160, 255)",
"sunflower": "rgb(0, 124, 0)",
"lilac": "rgb(0, 124, 0)",
"rose_bush": "rgb(0, 124, 0)",
"peony": "rgb(0, 124, 0)",
"tall_grass": "rgb(0, 124, 0)",
"large_fern": "rgb(0, 124, 0)",
"white_banner": "rgb(143, 119, 72)",
"orange_banner": "rgb(143, 119, 72)",
"magenta_banner": "rgb(143, 119, 72)",
"light_blue_banner": "rgb(143, 119, 72)",
"yellow_banner": "rgb(143, 119, 72)",
"lime_banner": "rgb(143, 119, 72)",
"pink_banner": "rgb(143, 119, 72)",
"gray_banner": "rgb(143, 119, 72)",
"light_gray_banner": "rgb(143, 119, 72)",
"cyan_banner": "rgb(143, 119, 72)",
"purple_banner": "rgb(143, 119, 72)",
"blue_banner": "rgb(143, 119, 72)",
"brown_banner": "rgb(143, 119, 72)",
"green_banner": "rgb(143, 119, 72)",
"red_banner": "rgb(143, 119, 72)",
"black_banner": "rgb(143, 119, 72)",
"white_wall_banner": "rgb(143, 119, 72)",
"orange_wall_banner": "rgb(143, 119, 72)",
"magenta_wall_banner": "rgb(143, 119, 72)",
"light_blue_wall_banner": "rgb(143, 119, 72)",
"yellow_wall_banner": "rgb(143, 119, 72)",
"lime_wall_banner": "rgb(143, 119, 72)",
"pink_wall_banner": "rgb(143, 119, 72)",
"gray_wall_banner": "rgb(143, 119, 72)",
"light_gray_wall_banner": "rgb(143, 119, 72)",
"cyan_wall_banner": "rgb(143, 119, 72)",
"purple_wall_banner": "rgb(143, 119, 72)",
"blue_wall_banner": "rgb(143, 119, 72)",
"brown_wall_banner": "rgb(143, 119, 72)",
"green_wall_banner": "rgb(143, 119, 72)",
"red_wall_banner": "rgb(143, 119, 72)",
"black_wall_banner": "rgb(143, 119, 72)",
"red_sandstone": "rgb(216, 127, 51)",
"chiseled_red_sandstone": "rgb(216, 127, 51)",
"cut_red_sandstone": "rgb(216, 127, 51)",
"oak_slab": "rgb(143, 119, 72)",
"spruce_slab": "rgb(129, 86, 49)",
"birch_slab": "rgb(247, 233, 163)",
"jungle_slab": "rgb(151, 109, 77)",
"acacia_slab": "rgb(216, 127, 51)",
"cherry_slab": "rgb(209, 177, 161)",
"dark_oak_slab": "rgb(102, 76, 51)",
"mangrove_slab": "rgb(153, 51, 51)",
"bamboo_slab": "rgb(229, 229, 51)",
"bamboo_mosaic_slab": "rgb(229, 229, 51)",
"stone_slab": "rgb(112, 112, 112)",
"smooth_stone_slab": "rgb(112, 112, 112)",
"sandstone_slab": "rgb(247, 233, 163)",
"cut_sandstone_slab": "rgb(247, 233, 163)",
"petrified_oak_slab": "rgb(143, 119, 72)",
"cobblestone_slab": "rgb(112, 112, 112)",
"brick_slab": "rgb(153, 51, 51)",
"stone_brick_slab": "rgb(112, 112, 112)",
"mud_brick_slab": "rgb(135, 107, 98)",
"nether_brick_slab": "rgb(112, 2, 0)",
"quartz_slab": "rgb(255, 252, 245)",
"red_sandstone_slab": "rgb(216, 127, 51)",
"cut_red_sandstone_slab": "rgb(216, 127, 51)",
"purpur_slab": "rgb(178, 76, 216)",
"smooth_stone": "rgb(112, 112, 112)",
"smooth_sandstone": "rgb(247, 233, 163)",
"smooth_quartz": "rgb(255, 252, 245)",
"smooth_red_sandstone": "rgb(216, 127, 51)",
"chorus_plant": "rgb(127, 63, 178)",
"chorus_flower": "rgb(127, 63, 178)",
"purpur_block": "rgb(178, 76, 216)",
"purpur_pillar": "rgb(178, 76, 216)",
"end_stone_bricks": "rgb(247, 233, 163)",
"torchflower_crop": "rgb(0, 124, 0)",
"pitcher_crop": "rgb(0, 124, 0)",
"pitcher_plant": "rgb(0, 124, 0)",
"beetroots": "rgb(0, 124, 0)",
"dirt_path": "rgb(151, 109, 77)",
"end_gateway": "rgb(25, 25, 25)",
"repeating_command_block": "rgb(127, 63, 178)",
"chain_command_block": "rgb(102, 127, 51)",
"frosted_ice": "rgb(160, 160, 255)",
"magma_block": "rgb(112, 2, 0)",
"nether_wart_block": "rgb(153, 51, 51)",
"red_nether_bricks": "rgb(112, 2, 0)",
"bone_block": "rgb(247, 233, 163)",
"observer": "rgb(112, 112, 112)",
"kelp": "rgb(64, 64, 255)",
"kelp_plant": "rgb(64, 64, 255)",
"dried_kelp_block": "rgb(102, 127, 51)",
"turtle_egg": "rgb(247, 233, 163)",
"sniffer_egg": "rgb(153, 51, 51)",
"dead_tube_coral_block": "rgb(76, 76, 76)",
"dead_brain_coral_block": "rgb(76, 76, 76)",
"dead_bubble_coral_block": "rgb(76, 76, 76)",
"dead_fire_coral_block": "rgb(76, 76, 76)",
"dead_horn_coral_block": "rgb(76, 76, 76)",
"tube_coral_block": "rgb(51, 76, 178)",
"brain_coral_block": "rgb(242, 127, 165)",
"bubble_coral_block": "rgb(127, 63, 178)",
"fire_coral_block": "rgb(153, 51, 51)",
"horn_coral_block": "rgb(229, 229, 51)",
"dead_tube_coral": "rgb(76, 76, 76)",
"dead_brain_coral": "rgb(76, 76, 76)",
"dead_bubble_coral": "rgb(76, 76, 76)",
"dead_fire_coral": "rgb(76, 76, 76)",
"dead_horn_coral": "rgb(76, 76, 76)",
"tube_coral": "rgb(51, 76, 178)",
"brain_coral": "rgb(242, 127, 165)",
"bubble_coral": "rgb(127, 63, 178)",
"fire_coral": "rgb(153, 51, 51)",
"horn_coral": "rgb(229, 229, 51)",
"dead_tube_coral_fan": "rgb(76, 76, 76)",
"dead_brain_coral_fan": "rgb(76, 76, 76)",
"dead_bubble_coral_fan": "rgb(76, 76, 76)",
"dead_fire_coral_fan": "rgb(76, 76, 76)",
"dead_horn_coral_fan": "rgb(76, 76, 76)",
"tube_coral_fan": "rgb(51, 76, 178)",
"brain_coral_fan": "rgb(242, 127, 165)",
"bubble_coral_fan": "rgb(127, 63, 178)",
"fire_coral_fan": "rgb(153, 51, 51)",
"horn_coral_fan": "rgb(229, 229, 51)",
"dead_tube_coral_wall_fan": "rgb(76, 76, 76)",
"dead_brain_coral_wall_fan": "rgb(76, 76, 76)",
"dead_bubble_coral_wall_fan": "rgb(76, 76, 76)",
"dead_fire_coral_wall_fan": "rgb(76, 76, 76)",
"dead_horn_coral_wall_fan": "rgb(76, 76, 76)",
"tube_coral_wall_fan": "rgb(51, 76, 178)",
"brain_coral_wall_fan": "rgb(242, 127, 165)",
"bubble_coral_wall_fan": "rgb(127, 63, 178)",
"fire_coral_wall_fan": "rgb(153, 51, 51)",
"horn_coral_wall_fan": "rgb(229, 229, 51)",
"sea_pickle": "rgb(102, 127, 51)",
"blue_ice": "rgb(160, 160, 255)",
"conduit": "rgb(92, 219, 213)",
"bamboo_sapling": "rgb(143, 119, 72)",
"bamboo": "rgb(0, 124, 0)",
"bubble_column": "rgb(64, 64, 255)",
"scaffolding": "rgb(247, 233, 163)",
"loom": "rgb(143, 119, 72)",
"barrel": "rgb(143, 119, 72)",
"smoker": "rgb(112, 112, 112)",
"blast_furnace": "rgb(112, 112, 112)",
"cartography_table": "rgb(143, 119, 72)",
"fletching_table": "rgb(143, 119, 72)",
"grindstone": "rgb(167, 167, 167)",
"lectern": "rgb(143, 119, 72)",
"smithing_table": "rgb(143, 119, 72)",
"stonecutter": "rgb(112, 112, 112)",
"bell": "rgb(250, 238, 77)",
"lantern": "rgb(167, 167, 167)",
"soul_lantern": "rgb(167, 167, 167)",
"campfire": "rgb(129, 86, 49)",
"soul_campfire": "rgb(129, 86, 49)",
"sweet_berry_bush": "rgb(0, 124, 0)",
"warped_hyphae": "rgb(86, 44, 62)",
"stripped_warped_hyphae": "rgb(86, 44, 62)",
"warped_nylium": "rgb(22, 126, 134)",
"warped_fungus": "rgb(76, 127, 153)",
"warped_wart_block": "rgb(20, 180, 133)",
"warped_roots": "rgb(76, 127, 153)",
"nether_sprouts": "rgb(76, 127, 153)",
"crimson_hyphae": "rgb(92, 25, 29)",
"stripped_crimson_hyphae": "rgb(92, 25, 29)",
"crimson_nylium": "rgb(189, 48, 49)",
"crimson_fungus": "rgb(112, 2, 0)",
"shroomlight": "rgb(153, 51, 51)",
"weeping_vines": "rgb(112, 2, 0)",
"weeping_vines_plant": "rgb(112, 2, 0)",
"twisting_vines": "rgb(76, 127, 153)",
"twisting_vines_plant": "rgb(76, 127, 153)",
"crimson_roots": "rgb(112, 2, 0)",
"crimson_planks": "rgb(148, 63, 97)",
"warped_planks": "rgb(58, 142, 140)",
"structure_block": "rgb(153, 153, 153)",
"jigsaw": "rgb(153, 153, 153)",
"composter": "rgb(143, 119, 72)",
"target": "rgb(255, 252, 245)",
"bee_nest": "rgb(229, 229, 51)",
"beehive": "rgb(143, 119, 72)",
"honey_block": "rgb(216, 127, 51)",
"honeycomb_block": "rgb(216, 127, 51)",
"netherite_block": "rgb(25, 25, 25)",
"ancient_debris": "rgb(25, 25, 25)",
"crying_obsidian": "rgb(25, 25, 25)",
"respawn_anchor": "rgb(25, 25, 25)",
"lodestone": "rgb(167, 167, 167)",
"blackstone": "rgb(25, 25, 25)",
"polished_blackstone_pressure_plate": "rgb(25, 25, 25)",
"chiseled_nether_bricks": "rgb(112, 2, 0)",
"cracked_nether_bricks": "rgb(112, 2, 0)",
"amethyst_block": "rgb(127, 63, 178)",
"budding_amethyst": "rgb(127, 63, 178)",
"amethyst_cluster": "rgb(127, 63, 178)",
"tuff": "rgb(57, 41, 35)",
"calcite": "rgb(209, 177, 161)",
"tinted_glass": "rgb(76, 76, 76)",
"powder_snow": "rgb(255, 255, 255)",
"sculk_sensor": "rgb(76, 127, 153)",
"sculk": "rgb(25, 25, 25)",
"sculk_vein": "rgb(25, 25, 25)",
"sculk_catalyst": "rgb(25, 25, 25)",
"sculk_shrieker": "rgb(25, 25, 25)",
"oxidized_copper": "rgb(22, 126, 134)",
"weathered_copper": "rgb(58, 142, 140)",
"exposed_copper": "rgb(135, 107, 98)",
"copper_block": "rgb(216, 127, 51)",
"deepslate_copper_ore": "rgb(100, 100, 100)",
"lightning_rod": "rgb(216, 127, 51)",
"pointed_dripstone": "rgb(76, 50, 35)",
"dripstone_block": "rgb(76, 50, 35)",
"cave_vines": "rgb(0, 124, 0)",
"cave_vines_plant": "rgb(0, 124, 0)",
"spore_blossom": "rgb(0, 124, 0)",
"azalea": "rgb(0, 124, 0)",
"flowering_azalea": "rgb(0, 124, 0)",
"moss_carpet": "rgb(102, 127, 51)",
"pink_petals": "rgb(0, 124, 0)",
"moss_block": "rgb(102, 127, 51)",
"big_dripleaf": "rgb(0, 124, 0)",
"big_dripleaf_stem": "rgb(0, 124, 0)",
"small_dripleaf": "rgb(0, 124, 0)",
"hanging_roots": "rgb(151, 109, 77)",
"rooted_dirt": "rgb(151, 109, 77)",
"mud": "rgb(87, 92, 92)",
"deepslate": "rgb(100, 100, 100)",
"infested_deepslate": "rgb(100, 100, 100)",
"raw_iron_block": "rgb(216, 175, 147)",
"raw_copper_block": "rgb(216, 127, 51)",
"raw_gold_block": "rgb(250, 238, 77)",
"ochre_froglight": "rgb(247, 233, 163)",
"verdant_froglight": "rgb(127, 167, 150)",
"pearlescent_froglight": "rgb(242, 127, 165)",
"frogspawn": "rgb(64, 64, 255)",
"reinforced_deepslate": "rgb(100, 100, 100)",
"decorated_pot": "rgb(142, 60, 46)"
}
}

View file

@ -16,3 +16,10 @@ export function chunkPos (pos: { x: number, z: number }) {
const z = Math.floor(pos.z / 16)
return [x, z]
}
export function sectionPos (pos: { x: number, y: number, z: number }) {
const x = Math.floor(pos.x / 16)
const y = Math.floor(pos.z / 16)
const z = Math.floor(pos.z / 16)
return [x, y, z]
}

View file

@ -48,6 +48,7 @@ export class WorldDataEmitter extends EventEmitter {
emitEntity(e)
},
entityUpdate: (e: any) => {
emitEntity(e)
},
entityMoved: (e: any) => {
emitEntity(e)

View file

@ -9,7 +9,7 @@ import { dispose3 } from './dispose'
import { toMajor } from './version.js'
import PrismarineChatLoader from 'prismarine-chat'
import { renderSign } from '../sign-renderer/'
import { chunkPos } from './simpleUtils'
import { chunkPos, sectionPos } from './simpleUtils'
function mod (x, n) {
return ((x % n) + n) % n
@ -260,7 +260,7 @@ export class WorldRenderer {
const loadBlockStates = async () => {
return new Promise(resolve => {
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
return loadJSON(`blocksStates/${this.texturesVersion}.json`, (data) => {
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
this.downloadedBlockStatesData = data
// todo
this.renderUpdateEmitter.emit('blockStatesDownloaded')
@ -275,12 +275,13 @@ export class WorldRenderer {
})
}
getLoadedChunksRelative (pos: Vec3) {
const [currentX, currentZ] = chunkPos(pos)
getLoadedChunksRelative (pos: Vec3, includeY = false) {
const [currentX, currentY, currentZ] = sectionPos(pos)
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
const [x, z] = chunkPos({ x: xRaw, z: zRaw })
return [`${x - currentX},${z - currentZ}`, o]
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
return [setKey, o]
}))
}

34
src/GlobalSearchInput.tsx Normal file
View file

@ -0,0 +1,34 @@
import { useSnapshot } from 'valtio'
import { miscUiState } from './globalState'
import Input from './react/Input'
function InnerSearch () {
const { currentTouch } = useSnapshot(miscUiState)
return <div style={{
position: 'fixed',
top: 5,
left: 0,
right: 0,
margin: 'auto',
zIndex: 11,
width: 'min-content',
}}>
<Input
autoFocus={currentTouch === false}
width={50}
placeholder='Search...'
defaultValue=""
onChange={({ target: { value } }) => {
customEvents.emit('search', value)
}}
/>
</div>
}
// todo remove component as its not possible to reuse this component atm
export default () => {
const { displaySearchInput } = useSnapshot(miscUiState)
return displaySearchInput ? <InnerSearch /> : null
}

View file

@ -1,5 +1,6 @@
import { options } from './optionsStorage'
import { isCypress } from './standaloneUtils'
import { reportWarningOnce } from './utils'
let audioContext: AudioContext
const sounds: Record<string, any> = {}
@ -39,7 +40,12 @@ export async function playSound (url, soundVolume = 1) {
if (!volume) return
audioContext ??= new window.AudioContext()
try {
audioContext ??= new window.AudioContext()
} catch (err) {
reportWarningOnce('audioContext', 'Failed to create audio context. Some sounds will not play')
return
}
for (const [soundName, sound] of Object.entries(sounds)) {
if (convertedSounds.includes(soundName)) continue

View file

@ -105,3 +105,15 @@ export const formatMessage = (message: MessageInput) => {
return msglist
}
const blockToItemRemaps = {
water: 'water_bucket',
lava: 'lava_bucket',
redstone_wire: 'redstone',
tripwire: 'tripwire_hook'
}
export const getItemFromBlock = (block: import('prismarine-block').Block) => {
const item = loadedData.items[blockToItemRemaps[block.name] ?? block.name]
return item
}

View file

@ -7,7 +7,7 @@ import * as browserfs from 'browserfs'
import { options, resetOptions } from './optionsStorage'
import { fsState, loadSave } from './loadSave'
import { installTexturePackFromHandle, updateTexturePackInstalledState } from './texturePack'
import { installTexturePack, installTexturePackFromHandle, updateTexturePackInstalledState } from './texturePack'
import { miscUiState } from './globalState'
import { setLoadingScreenStatus } from './utils'
@ -422,3 +422,42 @@ export const resetLocalStorageWithoutWorld = () => {
}
window.resetLocalStorageWorld = resetLocalStorageWorld
export const openFilePicker = (specificCase?: 'resourcepack') => {
// create and show input picker
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
if (!picker) {
picker = document.createElement('input')
picker.type = 'file'
picker.accept = '.zip'
picker.addEventListener('change', () => {
const file = picker.files?.[0]
picker.value = ''
if (!file) return
if (!file.name.endsWith('.zip')) {
const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? Only .zip files are supported. Continue?`)
if (!doContinue) return
}
if (specificCase === 'resourcepack') {
void installTexturePack(file)
} else {
void openWorldZip(file)
}
})
picker.hidden = true
document.body.appendChild(picker)
}
picker.click()
}
export const resetStateAfterDisconnect = () => {
miscUiState.gameLoaded = false
miscUiState.loadedDataVersion = null
miscUiState.singleplayer = false
miscUiState.flyingSquid = false
miscUiState.wanOpened = false
miscUiState.currentDisplayQr = null
fsState.saveLoaded = false
}

View file

@ -106,7 +106,7 @@ const commands: Array<{
{
command: ['/save'],
async invoke () {
await saveServer()
await saveServer(false)
}
}
]

View file

@ -14,6 +14,7 @@ import { chatInputValueGlobal } from './react/ChatContainer'
import { fsState } from './loadSave'
import { showOptionsModal } from './react/SelectOption'
import widgets from './react/widgets'
import { getItemFromBlock } from './botUtils'
// doesnt seem to work for now
const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
@ -67,6 +68,7 @@ export const contro = new ControMax({
},
gamepadPollingInterval: 10
})
window.controMax = contro
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
const setSprinting = (state: boolean) => {
@ -413,12 +415,12 @@ let allowFlying = false
export const onBotCreate = () => {
bot._client.on('abilities', ({ flags }) => {
allowFlying = !!(flags & 4)
if (flags & 2) { // flying
toggleFly(true, false)
} else {
toggleFly(false, false)
}
allowFlying = !!(flags & 4)
})
}
@ -443,7 +445,7 @@ const toggleFly = (newState = !isFlying(), sendAbilities?: boolean) => {
const selectItem = async () => {
const block = bot.blockAtCursor(5)
if (!block) return
const itemId = loadedData.itemsByName[block.name]?.id
const itemId = getItemFromBlock(block)?.id
if (!itemId) return
const Item = require('prismarine-item')(bot.version)
const item = new Item(itemId, 1, 0)

View file

@ -6,9 +6,9 @@ export default () => {
assertDefined(viewer)
// 0 morning
const dayTotal = 24_000
const evening = 12_542
const night = 17_843
const morningStart = 22_300
const evening = 11_500
const night = 13_500
const morningStart = 23_000
const morningEnd = 23_961
const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0

View file

@ -13,3 +13,6 @@ window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
window.cursorEntity = () => {
return getEntityCursor()
}
// wanderer
window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/9e487d23-2ffc-365a-b1f8-f38203f59233.dat').then(window.nbt.parse).then(console.log)

View file

@ -1,7 +1,10 @@
import { Entity } from 'prismarine-entity'
import tracker from '@nxg-org/mineflayer-tracker'
import { options, watchValue } from './optionsStorage'
customEvents.on('gameLoaded', () => {
bot.loadPlugin(tracker)
// todo cleanup (move to viewer, also shouldnt be used at all)
const playerPerAnimation = {} as Record<string, string>
const entityData = (e: Entity) => {
@ -10,6 +13,7 @@ customEvents.on('gameLoaded', () => {
window.debugEntityMetadata[e.username] = e
// todo entity spawn timing issue, check perf
if (viewer.entities.entities[e.id]?.playerObject) {
bot.tracker.trackEntity(e)
const { playerObject } = viewer.entities.entities[e.id]
playerObject.backEquipment = e.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
if (playerObject.cape.map === null) {
@ -17,18 +21,27 @@ customEvents.on('gameLoaded', () => {
}
// todo (easy, important) elytra flying animation
// todo cleanup states
const WALKING_SPEED = 0.1
const SPRINTING_SPEED = 0.15
const isWalking = Math.abs(e.velocity.x) > WALKING_SPEED || Math.abs(e.velocity.z) > WALKING_SPEED
const isSprinting = Math.abs(e.velocity.x) > SPRINTING_SPEED || Math.abs(e.velocity.z) > SPRINTING_SPEED
const newAnimation = isWalking ? (isSprinting ? 'running' : 'walking') : 'idle'
if (newAnimation !== playerPerAnimation[e.username]) {
viewer.entities.playAnimation(e.id, newAnimation)
playerPerAnimation[e.username] = newAnimation
}
}
}
bot.on('physicsTick', () => {
for (const [id, { tracking, info }] of Object.entries(bot.tracker.trackingData)) {
if (!tracking) continue
const e = bot.entities[id]!
const speed = info.avgSpeed
const WALKING_SPEED = 0.03
const SPRINTING_SPEED = 0.18
const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED
const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED
const newAnimation = isWalking ? (isSprinting ? 'running' : 'walking') : 'idle'
const username = e.username!
if (newAnimation !== playerPerAnimation[username]) {
viewer.entities.playAnimation(e.id, newAnimation)
playerPerAnimation[username] = newAnimation
}
}
})
bot.on('entitySwingArm', (e) => {
if (viewer.entities.entities[e.id]?.playerObject) {
viewer.entities.playAnimation(e.id, 'oneSwing')
@ -54,6 +67,7 @@ customEvents.on('gameLoaded', () => {
viewer.entities.addListener('remove', (e) => {
loadedSkinEntityIds.delete(e.id)
playerPerAnimation[e.username] = ''
bot.tracker.stopTrackingEntity(e, true)
})
bot.on('entityMoved', (e) => {

View file

@ -17,15 +17,25 @@ export function nameToMcOfflineUUID (name) {
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
}
export async function savePlayers () {
export async function savePlayers (autoSave: boolean) {
if (autoSave && new URL(location.href).searchParams.get('noSave') === 'true') return
//@ts-expect-error TODO
await localServer!.savePlayersSingleplayer()
}
// todo flying squid should expose save function instead
export const saveServer = async () => {
export const saveServer = async (autoSave = true) => {
if (!localServer || fsState.isReadonly) return
// todo
const worlds = [(localServer as any).overworld] as Array<import('prismarine-world').world.World>
await Promise.all([savePlayers(), ...worlds.map(async world => world.saveNow())])
await Promise.all([savePlayers(autoSave), ...worlds.map(async world => world.saveNow())])
}
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
bot.end('You left the server')
}

17
src/gameUnload.ts Normal file
View file

@ -0,0 +1,17 @@
import { subscribe } from 'valtio'
import { miscUiState } from './globalState'
let toCleanup = [] as Array<() => void>
export const watchUnloadForCleanup = (func: () => void) => {
toCleanup.push(func)
}
subscribe(miscUiState, () => {
if (!miscUiState.gameLoaded) {
for (const func of toCleanup) {
func()
}
toCleanup = []
}
})

View file

@ -0,0 +1,543 @@
export interface ClientWriteMap {
keep_alive: /** 1.7 */ {
keepAliveId: number;
} | /** 1.12.2 */ {
keepAliveId: bigint;
};
/** Removed in 1.19 */
chat: /** 1.7 */ {
message: string;
};
use_entity: /** 1.7 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
} | /** 1.9 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
hand: any;
} | /** 1.16 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
hand: any;
sneaking: boolean;
};
flying: /** 1.7 */ {
onGround: boolean;
};
position: /** 1.7 */ {
x: number;
stance: number;
y: number;
z: number;
onGround: boolean;
} | /** 1.8 */ {
x: number;
y: number;
z: number;
onGround: boolean;
};
look: /** 1.7 */ {
yaw: number;
pitch: number;
onGround: boolean;
};
position_look: /** 1.7 */ {
x: number;
stance: number;
y: number;
z: number;
yaw: number;
pitch: number;
onGround: boolean;
} | /** 1.8 */ {
x: number;
y: number;
z: number;
yaw: number;
pitch: number;
onGround: boolean;
};
block_dig: /** 1.7 */ {
status: number;
location: any;
face: number;
} | /** 1.8 */ {
status: number;
location: { x: number, y: number, z: number };
face: number;
} | /** 1.19 */ {
status: number;
location: { x: number, y: number, z: number };
face: number;
sequence: number;
};
block_place: /** 1.7 */ {
location: any;
direction: number;
heldItem: any;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.8 */ {
location: { x: number, y: number, z: number };
direction: number;
heldItem: any;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.9 */ {
location: { x: number, y: number, z: number };
direction: number;
hand: number;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.14 */ {
hand: number;
location: { x: number, y: number, z: number };
direction: number;
cursorX: number;
cursorY: number;
cursorZ: number;
insideBlock: boolean;
} | /** 1.19 */ {
hand: number;
location: { x: number, y: number, z: number };
direction: number;
cursorX: number;
cursorY: number;
cursorZ: number;
insideBlock: boolean;
sequence: number;
};
held_item_slot: /** 1.7 */ {
slotId: number;
};
arm_animation: /** 1.7 */ {
entityId: number;
animation: number;
} | /** 1.8 */ {
} | /** 1.9 */ {
hand: number;
};
entity_action: /** 1.7 */ {
entityId: number;
actionId: number;
jumpBoost: number;
};
steer_vehicle: /** 1.7 */ {
sideways: number;
forward: number;
jump: boolean;
unmount: boolean;
} | /** 1.8 */ {
sideways: number;
forward: number;
jump: number;
};
close_window: /** 1.7 */ {
windowId: number;
};
window_click: /** 1.7 */ {
windowId: number;
slot: number;
mouseButton: number;
action: number;
mode: number;
item: any;
} | /** 1.17 */ {
windowId: number;
slot: number;
mouseButton: number;
mode: number;
changedSlots: any;
cursorItem: any;
} | /** 1.17.1 */ {
windowId: number;
stateId: number;
slot: number;
mouseButton: number;
mode: number;
changedSlots: any;
cursorItem: any;
};
/** Removed in 1.17 */
transaction: /** 1.7 */ {
windowId: number;
action: number;
accepted: boolean;
};
set_creative_slot: /** 1.7 */ {
slot: number;
item: any;
};
enchant_item: /** 1.7 */ {
windowId: number;
enchantment: number;
};
update_sign: /** 1.7 */ {
location: any;
text1: string;
text2: string;
text3: string;
text4: string;
} | /** 1.8 */ {
location: { x: number, y: number, z: number };
text1: string;
text2: string;
text3: string;
text4: string;
} | /** 1.20 */ {
location: { x: number, y: number, z: number };
isFrontText: boolean;
text1: string;
text2: string;
text3: string;
text4: string;
};
abilities: /** 1.7 */ {
flags: number;
flyingSpeed: number;
walkingSpeed: number;
} | /** 1.16 */ {
flags: number;
};
tab_complete: /** 1.7 */ {
text: string;
} | /** 1.8 */ {
text: string;
block: any;
} | /** 1.9 */ {
text: string;
assumeCommand: boolean;
lookedAtBlock: any;
} | /** 1.13 */ {
transactionId: number;
text: string;
};
settings: /** 1.7 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
difficulty: number;
showCape: boolean;
} | /** 1.8 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
} | /** 1.9 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
} | /** 1.17 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
disableTextFiltering: boolean;
} | /** 1.18 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
enableTextFiltering: boolean;
enableServerListing: boolean;
};
client_command: /** 1.7 */ {
payload: number;
} | /** 1.9 */ {
actionId: number;
};
custom_payload: /** 1.7 */ {
channel: string;
data: any;
};
packet: /** 1.7 */ {
name: any;
params: any;
};
spectate: /** 1.8 */ {
target: any;
};
resource_pack_receive: /** 1.8 */ {
hash: string;
result: number;
} | /** 1.10 */ {
result: number;
} | /** 1.20.3 */ {
uuid: any;
result: number;
};
teleport_confirm: /** 1.9 */ {
teleportId: number;
};
vehicle_move: /** 1.9 */ {
x: number;
y: number;
z: number;
yaw: number;
pitch: number;
};
steer_boat: /** 1.9 */ {
leftPaddle: boolean;
rightPaddle: boolean;
};
use_item: /** 1.9 */ {
hand: number;
} | /** 1.19 */ {
hand: number;
sequence: number;
};
/** Removed in 1.12.1 */
prepare_crafting_grid: /** 1.12 */ {
windowId: number;
actionNumber: number;
returnEntry: any;
prepareEntry: any;
};
/** Removed in 1.16.2 */
crafting_book_data: /** 1.12 */ {
type: number;
undefined: any;
};
advancement_tab: /** 1.12 */ {
action: number;
tabId: any;
};
craft_recipe_request: /** 1.12.1 */ {
windowId: number;
recipe: number;
makeAll: boolean;
} | /** 1.13 */ {
windowId: number;
recipe: string;
makeAll: boolean;
};
query_block_nbt: /** 1.13 */ {
transactionId: number;
location: { x: number, y: number, z: number };
};
edit_book: /** 1.13 */ {
new_book: any;
signing: boolean;
} | /** 1.13.1 */ {
new_book: any;
signing: boolean;
hand: number;
} | /** 1.17.1 */ {
hand: number;
pages: any;
title: any;
};
query_entity_nbt: /** 1.13 */ {
transactionId: number;
entityId: number;
};
pick_item: /** 1.13 */ {
slot: number;
};
name_item: /** 1.13 */ {
name: string;
};
select_trade: /** 1.13 */ {
slot: number;
};
set_beacon_effect: /** 1.13 */ {
primary_effect: number;
secondary_effect: number;
} | /** 1.19 */ {
primary_effect: any;
secondary_effect: any;
};
update_command_block: /** 1.13 */ {
location: { x: number, y: number, z: number };
command: string;
mode: number;
flags: number;
};
update_command_block_minecart: /** 1.13 */ {
entityId: number;
command: string;
track_output: boolean;
};
update_structure_block: /** 1.13 */ {
location: { x: number, y: number, z: number };
action: number;
mode: number;
name: string;
offset_x: number;
offset_y: number;
offset_z: number;
size_x: number;
size_y: number;
size_z: number;
mirror: number;
rotation: number;
metadata: string;
integrity: number;
seed: any;
flags: number;
} | /** 1.19 */ {
location: { x: number, y: number, z: number };
action: number;
mode: number;
name: string;
offset_x: number;
offset_y: number;
offset_z: number;
size_x: number;
size_y: number;
size_z: number;
mirror: number;
rotation: number;
metadata: string;
integrity: number;
seed: number;
flags: number;
};
set_difficulty: /** 1.14 */ {
newDifficulty: number;
};
lock_difficulty: /** 1.14 */ {
locked: boolean;
};
update_jigsaw_block: /** 1.14 */ {
location: { x: number, y: number, z: number };
attachmentType: string;
targetPool: string;
finalState: string;
} | /** 1.16 */ {
location: { x: number, y: number, z: number };
name: string;
target: string;
pool: string;
finalState: string;
jointType: string;
} | /** 1.20.3 */ {
location: { x: number, y: number, z: number };
name: string;
target: string;
pool: string;
finalState: string;
jointType: string;
selection_priority: number;
placement_priority: number;
};
generate_structure: /** 1.16 */ {
location: { x: number, y: number, z: number };
levels: number;
keepJigsaws: boolean;
};
displayed_recipe: /** 1.16.2 */ {
recipeId: string;
};
recipe_book: /** 1.16.2 */ {
bookId: number;
bookOpen: boolean;
filterActive: boolean;
};
pong: /** 1.17 */ {
id: number;
};
chat_command: /** 1.19 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
signedPreview: boolean;
} | /** 1.19.2 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
signedPreview: boolean;
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
messageCount: number;
acknowledged: any;
};
chat_message: /** 1.19 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
signedPreview: boolean;
} | /** 1.19.2 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
signedPreview: boolean;
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
offset: number;
acknowledged: any;
};
/** Removed in 1.19.3 */
chat_preview: /** 1.19 */ {
query: number;
message: string;
};
message_acknowledgement: /** 1.19.2 */ {
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
count: number;
};
chat_session_update: /** 1.19.3 */ {
sessionUUID: any;
expireTime: bigint;
publicKey: any;
signature: any;
};
chunk_batch_received: /** 1.20.2 */ {
chunksPerTick: number;
};
/** Removed in 1.20.3 */
configuation_acknowledged: /** 1.20.2 */ {
};
ping_request: /** 1.20.2 */ {
id: bigint;
};
configuration_acknowledged: /** 1.20.3 */ {
};
set_slot_state: /** 1.20.3 */ {
slot_id: number;
window_id: number;
state: boolean;
};
}
export declare const clientWrite: <T extends keyof ClientWriteMap>(name: T, data: ClientWriteMap[T]) => Buffer

File diff suppressed because it is too large Load diff

35
src/globalDomListeners.ts Normal file
View file

@ -0,0 +1,35 @@
import { saveServer } from './flyingSquidUtils'
import { isGameActive, activeModalStack } from './globalState'
import { options } from './optionsStorage'
window.addEventListener('unload', (e) => {
if (!window.justReloaded) {
sessionStorage.justReloaded = false
}
void saveServer()
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') void saveServer()
})
document.addEventListener('blur', () => {
void saveServer()
})
window.addEventListener('beforeunload', (event) => {
if (!window.justReloaded) {
sessionStorage.justReloaded = false
}
// todo-low maybe exclude chat?
if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return
if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return
if (!options.closeConfirmation) return
// For major browsers doning only this is enough
event.preventDefault()
// Display a confirmation prompt
event.returnValue = '' // Required for some browsers
return 'The game is running. Are you sure you want to close this page?'
})

View file

@ -2,10 +2,7 @@
import { proxy, ref, subscribe } from 'valtio'
import { pointerLock } from './utils'
import { options } from './optionsStorage'
import type { OptionsGroupType } from './optionsGuiScheme'
import { saveServer } from './flyingSquidUtils'
import { fsState } from './loadSave'
// todo: refactor structure with support of hideNext=false
@ -141,20 +138,10 @@ export const miscUiState = proxy({
loadedDataVersion: null as string | null,
appLoaded: false,
usingGamepadInput: false,
appConfig: null as AppConfig | null
appConfig: null as AppConfig | null,
displaySearchInput: false,
})
export const resetStateAfterDisconnect = () => {
miscUiState.gameLoaded = false
miscUiState.loadedDataVersion = null
miscUiState.singleplayer = false
miscUiState.flyingSquid = false
miscUiState.wanOpened = false
miscUiState.currentDisplayQr = null
fsState.saveLoaded = false
}
export const isGameActive = (foregroundCheck: boolean) => {
if (foregroundCheck && activeModalStack.length) return false
return miscUiState.gameLoaded
@ -185,38 +172,3 @@ export const showNotification = (newNotification: Partial<typeof notification>)
}
// todo restore auto-save on interval for player data! (or implement it in flying squid since there is already auto-save for world)
window.addEventListener('unload', (e) => {
if (!window.justReloaded) {
sessionStorage.justReloaded = false
}
void saveServer()
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') void saveServer()
})
document.addEventListener('blur', () => {
void saveServer()
})
window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/9e487d23-2ffc-365a-b1f8-f38203f59233.dat').then(window.nbt.parse).then(console.log)
// todo move from global state
window.addEventListener('beforeunload', (event) => {
if (!window.justReloaded) {
sessionStorage.justReloaded = false
}
// todo-low maybe exclude chat?
if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return
if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return
if (!options.closeConfirmation) return
// For major browsers doning only this is enough
event.preventDefault()
// Display a confirmation prompt
event.returnValue = '' // Required for some browsers
return 'The game is running. Are you sure you want to close this page?'
})

16
src/globals.d.ts vendored
View file

@ -2,7 +2,12 @@
declare const THREE: typeof import('three')
// todo make optional
declare const bot: Omit<import('mineflayer').Bot, 'world'> & { world: import('prismarine-world').world.WorldSync }
declare const bot: Omit<import('mineflayer').Bot, 'world' | '_client'> & {
world: import('prismarine-world').world.WorldSync
_client: import('minecraft-protocol').Client & {
write: typeof import('./generatedClientPackets').clientWrite
}
}
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
@ -15,6 +20,7 @@ declare const customEvents: import('typed-emitter').default<{
singleplayer (): void
digStart ()
gameLoaded (): void
search (q: string): void
}>
declare const beforeRenderFrame: Array<() => void>
@ -65,3 +71,11 @@ declare module '*.png' {
const png: string
export default png
}
interface PromiseConstructor {
withResolvers<T> (): {
resolve: (value: T) => void;
reject: (reason: any) => void;
promise: Promise<T>;
}
}

View file

@ -1,2 +1,7 @@
// workaround for mineflayer
process.versions.node = '18.0.0'
if (!navigator.getGamepads) {
console.warn('navigator.getGamepads is not available, adding a workaround')
navigator.getGamepads ??= () => []
}

View file

@ -5,6 +5,7 @@ import './globals'
import 'iconify-icon'
import './devtools'
import './entities'
import './globalDomListeners'
import initCollisionShapes from './getCollisionShapes'
import { onGameLoad } from './playerWindows'
import { supportedVersions } from 'minecraft-protocol'
@ -22,6 +23,8 @@ import './menus/hud'
import './menus/play_screen'
import './menus/pause_screen'
import './menus/keybinds_screen'
import 'core-js/features/array/at'
import 'core-js/features/promise/with-resolvers'
import { initWithRenderer, statsEnd, statsStart } from './topRightStats'
import PrismarineBlock from 'prismarine-block'
import WebGpuRenderer from 'THREE/examples/jsm/renderers/webgpu/WebGPURenderer.js'
@ -30,7 +33,7 @@ import { options, watchValue } from './optionsStorage'
import './reactUi.jsx'
import { contro, onBotCreate } from './controls'
import './dragndrop'
import { possiblyCleanHandle } from './browserfs'
import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs'
import './eruda'
import { watchOptionsAfterViewerInit } from './watchOptions'
import downloadAndOpenFile from './downloadAndOpenFile'
@ -55,10 +58,11 @@ import {
showModal, activeModalStacks,
insertActiveModalStack,
isGameActive,
miscUiState, resetStateAfterDisconnect,
miscUiState,
notification
} from './globalState'
import {
pointerLock,
toMajorVersion,
@ -87,9 +91,12 @@ import { loadInMemorySave } from './react/SingleplayerProvider'
// side effects
import { downloadSoundsIfNeeded } from './soundSystem'
import { ua } from './react/utils'
import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasControls'
window.debug = debug
window.THREE = THREE
window.worldInteractions = worldInteractions
window.beforeRenderFrame = []
// ACTUAL CODE
@ -106,11 +113,19 @@ const renderer = new WebGpuRenderer({
}) as any
initWithRenderer(renderer.domElement)
window.renderer = renderer
renderer.setPixelRatio(window.devicePixelRatio || 1) // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
if (!renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
renderer.setPixelRatio(pixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.domElement.id = 'viewer-canvas'
document.body.appendChild(renderer.domElement)
const isFirefox = ua.getBrowser().name === 'Firefox'
if (isFirefox) {
// set custom property
document.body.style.setProperty('--thin-if-firefox', 'thin')
}
// Create viewer
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers)
window.viewer = viewer
@ -119,6 +134,11 @@ Object.defineProperty(window, 'debugSceneChunks', {
return viewer.world.getLoadedChunksRelative(bot.entity.position)
},
})
Object.defineProperty(window, 'debugSceneChunksY', {
get () {
return viewer.world.getLoadedChunksRelative(bot.entity.position, true)
},
})
viewer.entities.entitiesOptions = {
fontFamily: 'mojangles'
}
@ -331,12 +351,12 @@ async function connect (connectOptions: {
}
const handleError = (err) => {
errorAbortController.abort()
console.log('Encountered error!', err)
if (isCypress()) throw err
if (miscUiState.gameLoaded) return
setLoadingScreenStatus(`Error encountered. ${err}`, true)
onPossibleErrorDisconnect()
destroyAll()
if (isCypress()) throw err
}
const errorAbortController = new AbortController()
@ -651,6 +671,7 @@ async function connect (connectOptions: {
let screenTouches = 0
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | undefined
registerListener(document, 'pointerdown', (e) => {
const usingJoystick = options.touchControlsType === 'joystick-buttons'
const clickedEl = e.composedPath()[0]
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) {
return
@ -660,6 +681,16 @@ async function connect (connectOptions: {
// todo needs fixing!
// window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
}
if (usingJoystick) {
if (!joystickPointer.pointer && e.clientX < window.innerWidth / 2) {
joystickPointer.pointer = {
pointerId: e.pointerId,
x: e.clientX,
y: e.clientY
}
return
}
}
if (capturedPointer) {
return
}
@ -673,19 +704,33 @@ async function connect (connectOptions: {
activateCameraMove: false,
time: Date.now()
}
virtualClickTimeout ??= setTimeout(() => {
virtualClickActive = true
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
}, touchStartBreakingBlockMs)
if (options.touchControlsType !== 'joystick-buttons') {
virtualClickTimeout ??= setTimeout(() => {
virtualClickActive = true
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
}, touchStartBreakingBlockMs)
}
})
registerListener(document, 'pointermove', (e) => {
if (e.pointerId === undefined || e.pointerId !== capturedPointer?.id) return
if (e.pointerId === undefined) return
const supportsPressure = (e as any).pressure !== undefined && (e as any).pressure !== 0 && (e as any).pressure !== 0.5 && (e as any).pressure !== 1 && (e.pointerType === 'touch' || e.pointerType === 'pen')
if (e.pointerId === joystickPointer.pointer?.pointerId) {
handleMovementStickDelta(e)
if (supportsPressure && (e as any).pressure > 0.5) {
bot.setControlState('sprint', true)
// todo
}
return
}
if (e.pointerId !== capturedPointer?.id) return
window.scrollTo(0, 0)
e.preventDefault()
e.stopPropagation()
const allowedJitter = 1.1
// todo support .pressure (3d touch)
if (supportsPressure) {
bot.setControlState('jump', (e as any).pressure > 0.5)
}
const xDiff = Math.abs(e.pageX - capturedPointer.sourceX) > allowedJitter
const yDiff = Math.abs(e.pageY - capturedPointer.sourceY) > allowedJitter
if (!capturedPointer.activateCameraMove && (xDiff || yDiff)) capturedPointer.activateCameraMove = true
@ -698,18 +743,26 @@ async function connect (connectOptions: {
}, { passive: false })
const pointerUpHandler = (e: PointerEvent) => {
if (e.pointerId === undefined || e.pointerId !== capturedPointer?.id) return
if (e.pointerId === undefined) return
if (e.pointerId === joystickPointer.pointer?.pointerId) {
handleMovementStickDelta()
joystickPointer.pointer = null
return
}
if (e.pointerId !== capturedPointer?.id) return
clearTimeout(virtualClickTimeout)
virtualClickTimeout = undefined
if (virtualClickActive) {
// button 0 is left click
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
virtualClickActive = false
} else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) {
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
worldInteractions.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
if (options.touchControlsType !== 'joystick-buttons') {
if (virtualClickActive) {
// button 0 is left click
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
virtualClickActive = false
} else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) {
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
worldInteractions.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
}
}
capturedPointer = undefined
screenTouches--

View file

@ -4,11 +4,11 @@ import * as nbt from 'prismarine-nbt'
import { proxy } from 'valtio'
import { gzip } from 'node-gzip'
import { options } from './optionsStorage'
import { nameToMcOfflineUUID } from './flyingSquidUtils'
import { nameToMcOfflineUUID, disconnect } from './flyingSquidUtils'
import { forceCachedDataPaths } from './browserfs'
import { disconnect, isMajorVersionGreater } from './utils'
import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack, miscUiState } from './globalState'
import { appStatusState } from './react/AppStatusProvider'
import { isMajorVersionGreater } from './utils'
import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState'
// todo include name of opened handle (zip)!
// additional fs metadata

View file

@ -30,7 +30,7 @@ const commonCss = css`
/** @returns {boolean} */
function isMobile () {
return window.matchMedia('(pointer: coarse)').matches
return window.matchMedia('(pointer: coarse)').matches || navigator.userAgent.includes('Mobile')
}
// todo there are better workarounds and proper way to detect notch
@ -48,7 +48,7 @@ function openURL (url, newTab = true) {
if (newTab) {
window.open(url, '_blank', 'noopener,noreferrer')
} else {
window.open(url)
window.open(url, '_self')
}
}

View file

@ -93,11 +93,15 @@ class HealthBar extends LitElement {
}
effectAdded (effect) {
this.shadowRoot.querySelector('#health').classList.add(getEffectClass(effect))
const effectClass = getEffectClass(effect)
if (!effectClass) return
this.shadowRoot.querySelector('#health').classList.add(effectClass)
}
effectEnded (effect) {
this.shadowRoot.querySelector('#health').classList.remove(getEffectClass(effect))
const effectClass = getEffectClass(effect)
if (!effectClass) return
this.shadowRoot.querySelector('#health').classList.remove(effectClass)
}
onDamage () {

View file

@ -117,12 +117,8 @@ class Hud extends LitElement {
this.isReady = true
window.dispatchEvent(new CustomEvent('hud-ready', { detail: this }))
watchValue(options, (o) => {
miscUiState.currentTouch = o.alwaysShowMobileControls || isMobile()
this.showMobileControls(miscUiState.currentTouch)
})
watchValue(miscUiState, o => {
this.showMobileControls(o.currentTouch)
//@ts-expect-error
this.shadowRoot.host.style.display = o.gameLoaded ? 'block' : 'none'
})

View file

@ -4,7 +4,8 @@ const { subscribe } = require('valtio')
const { subscribeKey } = require('valtio/utils')
const { hideCurrentModal, showModal, miscUiState, notification, openOptionsMenu } = require('../globalState')
const { fsState } = require('../loadSave')
const { disconnect, openGithub } = require('../utils')
const { openGithub } = require('../utils')
const { disconnect } = require('../flyingSquidUtils')
const { closeWan, openToWanAndCopyJoinLink, getJoinLink } = require('../localServerMultiplayer')
const { uniqueFileNameFromWorldName, copyFilesAsyncWithProgress } = require('../browserfs')
const { showOptionsModal } = require('../react/SelectOption')

View file

@ -6,9 +6,10 @@ import { AppOptions, options } from './optionsStorage'
import Button from './react/Button'
import { OptionMeta, OptionSlider } from './react/OptionsItems'
import Slider from './react/Slider'
import { getScreenRefreshRate, openFilePicker, setLoadingScreenStatus } from './utils'
import { getScreenRefreshRate, setLoadingScreenStatus } from './utils'
import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs'
import { getResourcePackName, resourcePackState, uninstallTexturePack } from './texturePack'
import { resetLocalStorageWithoutWorld } from './browserfs'
export const guiOptionsScheme: {
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom?}>
@ -44,11 +45,7 @@ export const guiOptionsScheme: {
},
{
custom () {
return <>
<div></div>
<span style={{ fontSize: 9, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>Experimental</span>
<div></div>
</>
return <Category>Experimental</Category>
},
dayCycleAndLighting: {
text: 'Day Cycle',
@ -138,6 +135,9 @@ export const guiOptionsScheme: {
unit: '',
delayApply: true,
},
custom () {
return <Category>Chat</Category>
},
chatWidth: {
max: 320,
unit: 'px',
@ -150,6 +150,8 @@ export const guiOptionsScheme: {
},
chatOpacityOpened: {
},
chatSelect: {
},
}
],
controls: [
@ -186,7 +188,16 @@ export const guiOptionsScheme: {
},
touchButtonsPosition: {
max: 80
}
},
touchControlsType: {
values: [['classic', 'Classic'], ['joystick-buttons', 'New']],
},
},
{
custom () {
const { touchControlsType } = useSnapshot(options)
return <Button label='Setup Touch Buttons' onClick={() => showModal({ reactType: 'touch-buttons-setup' })} inScreen disabled={touchControlsType !== 'joystick-buttons'} />
},
}
],
sound: [
@ -219,3 +230,9 @@ export const guiOptionsScheme: {
],
}
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR'
const Category = ({ children }) => <div style={{
fontSize: 9,
textAlign: 'center',
gridColumn: 'span 2'
}}>{children}</div>

View file

@ -29,6 +29,21 @@ const defaultOptions = {
touchButtonsSize: 40,
touchButtonsOpacity: 80,
touchButtonsPosition: 12,
touchControlsPositions: {
action: [
90,
70
],
sneak: [
90,
90
],
break: [
70,
70
]
} as Record<string, [number, number]>,
touchControlsType: 'classic' as 'classic' | 'joystick-buttons',
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
/** @unstable */
disableAssets: false,
@ -54,17 +69,23 @@ const defaultOptions = {
/** Actually might be useful */
showCursorBlockInSpectator: false,
renderEntities: true,
chatSelect: false,
// advanced bot options
autoRespawn: false,
mutedSounds: [] as string[]
mutedSounds: [] as string[],
plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>,
}
const migrateOptions = (options) => {
const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
if (options.highPerformanceGpu) {
options.gpuPreference = 'high-performance'
delete options.highPerformanceGpu
}
if (Object.keys(options.touchControlsPositions ?? {}).length === 0) {
options.touchControlsPositions = defaultOptions.touchControlsPositions
}
return options
}

View file

@ -6,10 +6,16 @@ import LargeChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui
import FurnaceGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/furnace.png'
import CraftingTableGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/crafting_table.png'
import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/dispenser.png'
import HopperGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/hopper.png'
import HorseGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/horse.png'
import VillagerGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/villager2.png'
import EnchantingGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/enchanting_table.png'
import AnvilGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/anvil.png'
import BeaconGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/beacon.png'
import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png'
import { subscribeKey } from 'valtio/utils'
import MinecraftData from 'minecraft-data'
import MinecraftData, { RecipeItem } from 'minecraft-data'
import { getVersion } from 'prismarine-viewer/viewer/lib/version'
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
import itemsPng from 'prismarine-viewer/public/textures/items.png'
@ -20,12 +26,15 @@ import PrismarineBlockLoader from 'prismarine-block'
import { flat } from '@xmcl/text-component'
import mojangson from 'mojangson'
import nbt from 'prismarine-nbt'
import { splitEvery, equals } from 'rambda'
import PItem, { Item } from 'prismarine-item'
import Generic95 from '../assets/generic_95.png'
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState'
import invspriteJson from './invsprite.json'
import { options } from './optionsStorage'
import { assertDefined } from './utils'
const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
export const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
const loadedImagesCache = new Map<string, HTMLImageElement>()
const cleanLoadedImagesCache = () => {
loadedImagesCache.delete('blocks')
@ -53,6 +62,7 @@ let lastWindow
/** bot version */
let version: string
let PrismarineBlock: typeof PrismarineBlockLoader.Block
let PrismarineItem: typeof Item
export const onGameLoad = (onLoad) => {
let loaded = 0
@ -65,6 +75,7 @@ export const onGameLoad = (onLoad) => {
getImage({ path: 'items' }, onImageLoaded)
getImage({ path: 'items-legacy' }, onImageLoaded)
PrismarineBlock = PrismarineBlockLoader(version)
PrismarineItem = PItem(version)
bot.on('windowOpen', (win) => {
if (implementedContainersGuiMap[win.type]) {
@ -76,12 +87,44 @@ export const onGameLoad = (onLoad) => {
// todo format
bot._client.emit('chat', {
message: JSON.stringify({
text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Items: ${win.slots.map(slot => slot?.name).join(', ')}`
text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item) ?? '(empty)').join(', ')}`
})
})
bot.currentWindow?.['close']()
}
})
bot.inventory.on('updateSlot', ((_oldSlot, oldItem, newItem) => {
const oldSlot = _oldSlot as number
if (!miscUiState.singleplayer) return
const { craftingResultSlot } = bot.inventory
if (oldSlot === craftingResultSlot && oldItem && !newItem) {
for (let i = 1; i < 5; i++) {
const count = bot.inventory.slots[i]?.count
if (count && count > 1) {
const slot = bot.inventory.slots[i]!
slot.count--
void bot.creative.setInventorySlot(i, slot)
} else {
void bot.creative.setInventorySlot(i, null)
}
}
return
}
const craftingSlots = bot.inventory.slots.slice(1, 5)
const resultingItem = getResultingRecipe(craftingSlots, 2)
void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null)
}) as any)
bot.on('windowClose', () => {
// todo hide up to the window itself!
hideCurrentModal()
})
customEvents.on('search', (q) => {
if (!lastWindow) return
upJei(q)
})
}
const findTextureInBlockStates = (name) => {
@ -148,6 +191,13 @@ const getImageSrc = (path): string | HTMLImageElement => {
case 'gui/container/crafting_table': return CraftingTableGui
case 'gui/container/shulker_box': return ChestLikeGui
case 'gui/container/generic_54': return LargeChestLikeGui
case 'gui/container/generic_95': return Generic95
case 'gui/container/hopper': return HopperGui
case 'gui/container/horse': return HorseGui
case 'gui/container/villager2': return VillagerGui
case 'gui/container/enchanting_table': return EnchantingGui
case 'gui/container/anvil': return AnvilGui
case 'gui/container/beacon': return BeaconGui
}
return Dirt
}
@ -192,7 +242,8 @@ const isFullBlock = (block: string) => {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
type RenderSlot = Pick<import('prismarine-item').Item, 'name' | 'displayName' | 'durabilityUsed' | 'maxDurability' | 'enchants'>
const renderSlot = (slot: RenderSlot, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
const itemName = slot.name
const isItem = loadedData.itemsByName[itemName]
const fullBlock = isFullBlock(itemName)
@ -239,8 +290,8 @@ type PossibleItemProps = {
Damage?: number
display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"}
}
export const getItemName = (item: import('prismarine-item').Item) => {
if (!item.nbt) return
export const getItemName = (item: import('prismarine-item').Item | null) => {
if (!item?.nbt) return
const itemNbt: PossibleItemProps = nbt.simplify(item.nbt)
const customName = itemNbt.display?.Name
if (!customName) return
@ -260,22 +311,25 @@ export const renderSlotExternal = (slot) => {
}
}
const upInventory = (inventory: boolean) => {
// inv.pwindow.inv.slots[2].displayName = 'test'
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
const updateSlots = (inventory ? bot.inventory : bot.currentWindow)!.slots.map(slot => {
const mapSlots = (slots: Array<RenderSlot | Item | null>) => {
return slots.map(slot => {
// todo stateid
if (!slot) return
try {
const slotCustomProps = renderSlot(slot)
Object.assign(slot, { ...slotCustomProps, displayName: getItemName(slot) ?? slot.displayName })
Object.assign(slot, { ...slotCustomProps, displayName: ('nbt' in slot ? getItemName(slot) : undefined) ?? slot.displayName })
} catch (err) {
console.error(err)
}
return slot
})
const customSlots = updateSlots
}
const upInventory = (isInventory: boolean) => {
// inv.pwindow.inv.slots[2].displayName = 'test'
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
const customSlots = mapSlots((isInventory ? bot.inventory : bot.currentWindow)!.slots)
lastWindow.pwindow.setSlots(customSlots)
}
@ -292,11 +346,31 @@ export const onModalClose = (callback: () => any) => {
const implementedContainersGuiMap = {
// todo allow arbitrary size instead!
'minecraft:generic_9x3': 'ChestWin',
'minecraft:generic_9x5': 'Generic95Win',
// hopper
'minecraft:generic_5x1': 'HopperWin',
'minecraft:generic_9x6': 'LargeChestWin',
'minecraft:generic_3x3': 'DropDispenseWin',
'minecraft:furnace': 'FurnaceWin',
'minecraft:smoker': 'FurnaceWin',
'minecraft:crafting': 'CraftingWin'
'minecraft:crafting': 'CraftingWin',
'minecraft:anvil': 'AnvilWin',
// enchant
'minecraft:enchanting_table': 'EnchantingWin',
// horse
'minecraft:horse': 'HorseWin',
// villager
'minecraft:villager': 'VillagerWin',
}
const upJei = (search: string) => {
search = search.toLowerCase()
// todo fix pre flat
const matchedSlots = loadedData.itemsArray.map(x => {
if (!x.displayName.toLowerCase().includes(search)) return null!
return new PrismarineItem(x.id, 1)
}).filter(Boolean)
lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots)
}
const openWindow = (type: string | undefined) => {
@ -313,6 +387,7 @@ const openWindow = (type: string | undefined) => {
if (type !== undefined && bot.currentWindow) bot.currentWindow['close']()
lastWindow.destroy()
lastWindow = null
miscUiState.displaySearchInput = false
destroyFn()
})
cleanLoadedImagesCache()
@ -321,7 +396,7 @@ const openWindow = (type: string | undefined) => {
inv.canvas.style.position = 'fixed'
inv.canvas.style.inset = '0'
// todo scaling
inv.canvasManager.setScale(window.innerHeight < 480 ? 2 : window.innerHeight < 700 ? 3 : 4)
inv.canvasManager.setScale(window.innerWidth < 470 ? 1.5 : window.innerHeight < 480 || window.innerWidth < 760 ? 2 : window.innerHeight < 700 ? 3 : 4)
inv.canvasManager.onClose = () => {
hideCurrentModal()
@ -330,10 +405,35 @@ const openWindow = (type: string | undefined) => {
lastWindow = inv
const upWindowItems = () => {
upInventory(type === undefined)
void Promise.resolve().then(() => upInventory(type === undefined))
}
upWindowItems()
lastWindow.pwindow.touch = miscUiState.currentTouch
lastWindow.pwindow.onJeiClick = (slotItem, _index, isRightclick) => {
// slotItem is the slot from mapSlots
const itemId = loadedData.itemsByName[slotItem.name]?.id
if (!itemId) {
console.error(`Item for block ${slotItem.name} not found`)
return
}
const item = new PrismarineItem(itemId, isRightclick ? 64 : 1, slotItem.metadata)
const freeSlot = bot.inventory.firstEmptyInventorySlot()
if (freeSlot === null) return
void bot.creative.setInventorySlot(freeSlot, item)
}
if (bot.game.gameMode === 'creative') {
lastWindow.pwindow.win.jeiSlotsPage = 0
// todo workaround so inventory opens immediately (but still lags)
setTimeout(() => {
upJei('')
})
miscUiState.displaySearchInput = true
} else {
lastWindow.pwindow.win.jeiSlots = []
}
if (type === undefined) {
// player inventory
bot.inventory.on('updateSlot', upWindowItems)
@ -341,10 +441,6 @@ const openWindow = (type: string | undefined) => {
bot.inventory.off('updateSlot', upWindowItems)
}
} else {
bot.on('windowClose', () => {
// todo hide up to the window itself!
hideCurrentModal()
})
//@ts-expect-error
bot.currentWindow.on('updateSlot', () => {
upWindowItems()
@ -357,3 +453,49 @@ let destroyFn = () => { }
export const openPlayerInventory = () => {
openWindow(undefined)
}
const getResultingRecipe = (slots: Array<Item | null>, gridRows: number) => {
const inputSlotsItems = slots.map(blockSlot => blockSlot?.type)
let currentShape = splitEvery(gridRows, inputSlotsItems as Array<number | undefined | null>)
// todo rewrite with candidates search
if (currentShape.length > 1) {
// eslint-disable-next-line @typescript-eslint/no-for-in-array
for (const slotX in currentShape[0]) {
if (currentShape[0][slotX] !== undefined) {
for (const [otherY] of Array.from({ length: gridRows }).entries()) {
if (currentShape[otherY]?.[slotX] === undefined) {
currentShape[otherY]![slotX] = null
}
}
}
}
}
currentShape = currentShape.map(arr => arr.filter(x => x !== undefined)).filter(x => x.length !== 0)
// todo rewrite
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
const slotsIngredients = [...inputSlotsItems].sort().filter(item => item !== undefined)
type Result = RecipeItem | undefined
let shapelessResult: Result
let shapeResult: Result
outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) {
for (const recipeVariant of recipeVariants) {
if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) {
shapeResult = recipeVariant.result!
break outer
}
if ('ingredients' in recipeVariant && equals(slotsIngredients, recipeVariant.ingredients?.sort() as number[])) {
shapelessResult = recipeVariant.result
break outer
}
}
}
const result = shapeResult ?? shapelessResult
if (!result) return
const id = typeof result === 'number' ? result : Array.isArray(result) ? result[0] : result.id
if (!id) return
const count = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1
const metadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined
const item = new PrismarineItem(id, count, metadata)
return item
}

View file

@ -30,7 +30,9 @@ export default ({ status, isError, hideDots = false, lastStatus = '', backAction
<Screen
title={
<>
{status}
<span style={{ userSelect: isError ? 'text' : undefined }}>
{status}
</span>
{isError || hideDots ? '' : loadingDots}
<p className={styles['potential-problem']}>{description}</p>
<p className={styles['last-status']}>{lastStatus ? `Last status: ${lastStatus}` : lastStatus}</p>

View file

@ -95,6 +95,7 @@ type Story = StoryObj<typeof Chat>
export const Primary: Story = {
args: {
usingTouch: false,
messages: [{
parts: [
{

View file

@ -122,7 +122,7 @@ div.chat-wrapper {
pointer-events: none;
overflow: hidden;
width: 100%;
scrollbar-width: thin;
scrollbar-width: var(--thin-if-firefox);
}
.chat.opened {

View file

@ -1,10 +1,11 @@
import { useUsingTouch } from '@dimaka/interface'
import { proxy, subscribe } from 'valtio'
import { proxy, subscribe, useSnapshot } from 'valtio'
import { useEffect, useMemo, useRef, useState } from 'react'
import { isCypress } from '../standaloneUtils'
import { MessageFormatPart } from '../botUtils'
import { miscUiState } from '../globalState'
import { MessagePart } from './MessageFormatted'
import './ChatContainer.css'
import { isIos } from './utils'
export type Message = {
parts: MessageFormatPart[],
@ -13,7 +14,7 @@ export type Message = {
faded?: boolean
}
const MessageLine = ({ message }: {message: Message}) => {
const MessageLine = ({ message }: { message: Message }) => {
const classes = {
'chat-message-fadeout': message.fading,
'chat-message-fade': message.fading,
@ -28,12 +29,14 @@ const MessageLine = ({ message }: {message: Message}) => {
type Props = {
messages: Message[]
usingTouch: boolean
opacity?: number
opened?: boolean
onClose?: () => void
sendMessage?: (message: string) => boolean | void
fetchCompletionItems?: (triggerKind: 'implicit' | 'explicit', completeValue: string, fullValue: string, abortController?: AbortController) => Promise<string[] | void>
// width?: number
allowSelection?: boolean
}
export const chatInputValueGlobal = proxy({
@ -51,9 +54,7 @@ export const fadeMessage = (message: Message, initialTimeout: boolean, requestUp
}, initialTimeout ? 5000 : 0)
}
export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessage, onClose }: Props) => {
const usingTouch = useUsingTouch()
export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessage, onClose, usingTouch, allowSelection }: Props) => {
const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]'))
const [completePadText, setCompletePadText] = useState('')
@ -200,12 +201,14 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa
return (
<>
<div className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''}`} hidden={isCypress()}>
<div className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''}`} hidden={isCypress()} style={{
userSelect: opened && allowSelection ? 'text' : undefined,
}}>
{opacity && <div ref={chatMessages} className={`chat ${opened ? 'opened' : ''}`} id="chat-messages" style={{ opacity }}>
{messages.map((m) => (
<MessageLine key={m.id} message={m} />
))}
</div>}
</div> || undefined}
</div>
<div className={`chat-wrapper chat-input-wrapper ${usingTouch ? 'input-mobile' : ''}`} hidden={!opened}>
@ -220,78 +223,81 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa
</div>
</div>
) : null}
<input
value=''
type="text"
className="chat-mobile-hidden"
id="chatinput-next-command"
spellCheck={false}
autoComplete="off"
onFocus={() => auxInputFocus('ArrowUp')}
onChange={() => { }}
/>
<input
defaultValue=''
ref={chatInput}
type="text"
className="chat-input"
id="chatinput"
spellCheck={false}
autoComplete="off"
aria-autocomplete="both"
onChange={onMainInputChange}
onKeyDown={(e) => {
if (e.code === 'ArrowUp') {
if (chatHistoryPos.current === 0) return
if (chatHistoryPos.current === sendHistoryRef.current.length) { // started navigating history
inputCurrentlyEnteredValue.current = e.currentTarget.value
}
chatHistoryPos.current--
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || '')
} else if (e.code === 'ArrowDown') {
if (chatHistoryPos.current === sendHistoryRef.current.length) return
chatHistoryPos.current++
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
<form onSubmit={(e) => {
e.preventDefault()
const message = chatInput.current.value
if (message) {
setSendHistory([...sendHistoryRef.current, message])
const result = sendMessage?.(message)
if (result !== false) {
onClose?.()
}
if (e.code === 'Tab') {
if (completionItemsSource.length) {
if (completionItems.length) {
acceptComplete(completionItems[0])
}
}}>
{isIos && <input
value=''
type="text"
className="chat-mobile-hidden"
id="chatinput-next-command"
spellCheck={false}
autoComplete="off"
onFocus={() => auxInputFocus('ArrowUp')}
onChange={() => { }}
/>}
<input
defaultValue=''
ref={chatInput}
type="text"
className="chat-input"
id="chatinput"
spellCheck={false}
autoComplete="off"
aria-autocomplete="both"
onChange={onMainInputChange}
onKeyDown={(e) => {
if (e.code === 'ArrowUp') {
if (chatHistoryPos.current === 0) return
if (chatHistoryPos.current === sendHistoryRef.current.length) { // started navigating history
inputCurrentlyEnteredValue.current = e.currentTarget.value
}
} else {
void fetchCompletions(false)
chatHistoryPos.current--
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || '')
} else if (e.code === 'ArrowDown') {
if (chatHistoryPos.current === sendHistoryRef.current.length) return
chatHistoryPos.current++
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
}
e.preventDefault()
}
if (e.code === 'Space') {
resetCompletionItems()
if (chatInput.current.value.startsWith('/')) {
// alternative we could just simply use keyup, but only with keydown we can display suggestions popup as soon as possible
void fetchCompletions(true, getCompleteValue(getDefaultCompleteValue() + ' '))
if (e.code === 'Tab') {
if (completionItemsSource.length) {
if (completionItems.length) {
acceptComplete(completionItems[0])
}
} else {
void fetchCompletions(false)
}
e.preventDefault()
}
}
if (e.code === 'Enter') {
const message = chatInput.current.value
if (message) {
setSendHistory([...sendHistoryRef.current, message])
const result = sendMessage?.(message)
if (result !== false) {
onClose?.()
if (e.code === 'Space') {
resetCompletionItems()
if (chatInput.current.value.startsWith('/')) {
// alternative we could just simply use keyup, but only with keydown we can display suggestions popup as soon as possible
void fetchCompletions(true, getCompleteValue(getDefaultCompleteValue() + ' '))
}
}
}
}}
/>
<input
value=''
type="text"
className="chat-mobile-hidden"
id="chatinput-prev-command"
spellCheck={false}
autoComplete="off"
onFocus={() => auxInputFocus('ArrowDown')}
onChange={() => { }}
/>
}}
/>
{isIos && <input
value=''
type="text"
className="chat-mobile-hidden"
id="chatinput-prev-command"
spellCheck={false}
autoComplete="off"
onFocus={() => auxInputFocus('ArrowDown')}
onChange={() => { }}
/>}
<button type='submit' style={{ visibility: 'hidden' }} />
</form>
</div>
</div>
</>

View file

@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { useSnapshot } from 'valtio'
import { formatMessage } from '../botUtils'
import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands'
import { hideCurrentModal } from '../globalState'
import { hideCurrentModal, miscUiState } from '../globalState'
import { options } from '../optionsStorage'
import ChatContainer, { Message, fadeMessage } from './ChatContainer'
import { useIsModalActive } from './utils'
@ -11,6 +12,8 @@ export default () => {
const isChatActive = useIsModalActive('chat')
const { messagesLimit, chatOpacity, chatOpacityOpened } = options
const lastMessageId = useRef(0)
const usingTouch = useSnapshot(miscUiState).currentTouch
const { chatSelect } = useSnapshot(options)
useEffect(() => {
bot.addListener('message', (jsonMsg, position) => {
@ -33,6 +36,8 @@ export default () => {
}, [])
return <ChatContainer
allowSelection={chatSelect}
usingTouch={!!usingTouch}
opacity={(isChatActive ? chatOpacityOpened : chatOpacity) / 100}
messages={messages}
opened={isChatActive}

View file

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react'
import Button from './Button'
const defaultIcon = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 21H5H19H21V3H19H5H3V21ZM19 5V19H5V5H19ZM11 17H13V11H15V9H13V7H11V9H9V11H11V17ZM9 13V11H7V13H9ZM17 13H15V11H17V13Z" fill="currentColor"></path></svg>
const Button2 = ({ title, icon }) => {
//@ts-expect-error
return <Button style={{ '--scale': 4 }}>
<div style={{ fontSize: '22px', fontWeight: 'bold', display: 'flex', gap: 3, flexDirection: 'column', alignItems: 'center' }}>
<div>
{title}
</div>
{/* <iconify-icon icon="pixelarticons: */}
<div style={{ width: 30, height: 30 }} className='full-svg'>
{icon}
</div>
</div>
</Button>
}
const Comp = () => {
return <div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 10
}}>
<Button2 title="/give" icon={defaultIcon} />
<Button2 title="/tell" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 2h18v16H6v2H4v-2h2v-2h14V4H4v18H2V2h2zm5 7H7v2h2V9zm2 0h2v2h-2V9zm6 0h-2v2h2V9z" fill="currentColor" /> </svg>} />
<Button2 title="/setblock" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M2 2h20v20H2V2zm2 2v4h4V4H4zm6 0v4h4V4h-4zm6 0v4h4V4h-4zm4 6h-4v4h4v-4zm0 6h-4v4h4v-4zm-6 4v-4h-4v4h4zm-6 0v-4H4v4h4zm-4-6h4v-4H4v4zm6-4v4h4v-4h-4z" fill="currentColor" /> </svg>} />
<Button2 title="/tp" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16 5H2v14h14v-2h2v-2h2v-2h2v-2h-2V9h-2V7h-2V5zm0 2v2h2v2h2v2h-2v2h-2v2H4V7h12z" fill="currentColor" /> </svg>} />
<Button2 title="/clone" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M5 3H3v2h2V3zm2 4h2v2H7V7zm4 0h2v2h-2V7zm2 12h-2v2h2v-2zm2 0h2v2h-2v-2zm6 0h-2v2h2v-2zM7 11h2v2H7v-2zm14 0h-2v2h2v-2zm-2 4h2v2h-2v-2zM7 19h2v2H7v-2zM19 7h2v2h-2V7zM7 3h2v2H7V3zm2 12H7v2h2v-2zM3 7h2v2H3V7zm14 0h-2v2h2V7zM3 11h2v2H3v-2zm2 4H3v2h2v-2zm6-12h2v2h-2V3zm6 0h-2v2h2V3z" fill="currentColor" /> </svg>} />
<Button2 title="/fill" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M21 3h-8v2h4v2h2v4h2V3zm-4 4h-2v2h-2v2h2V9h2V7zm-8 8h2v-2H9v2H7v2h2v-2zm-4-2v4h2v2H5h6v2H3v-8h2z" fill="currentColor" /> </svg>} />
<Button2 title="/home" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M14 2h-4v2H8v2H6v2H4v2H2v2h2v10h7v-6h2v6h7V12h2v-2h-2V8h-2V6h-2V4h-2V2zm0 2v2h2v2h2v2h2v2h-2v8h-3v-6H9v6H6v-8H4v-2h2V8h2V6h2V4h4z" fill="currentColor" /> </svg>} />
<Button2 title="/time" icon={<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path d="M20 0h2v2h2v2h-2v2h-2V4h-2V2h2V0ZM8 4h8v2h-2v2h-2V6H8V4ZM6 8V6h2v2H6Zm0 8H4V8h2v8Zm2 2H6v-2h2v2Zm8 0v2H8v-2h8Zm2-2v2h-2v-2h2Zm-2-4v-2h2V8h2v8h-2v-4h-2Zm-4 0h4v2h-4v-2Zm0 0V8h-2v4h2Zm-8 6H2v2H0v2h2v2h2v-2h2v-2H4v-2Z" /> </svg>} />
<Button2 title="/gamerule" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 5h16v2H4V5zm0 12H2V7h2v10zm16 0v2H4v-2h16zm0 0h2V7h-2v10zm-2-8h-4v6h4V9z" fill="currentColor" /> </svg>} />
<Button2 title="/vanish" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M8 6h8v2H8V6zm-4 4V8h4v2H4zm-2 2v-2h2v2H2zm0 2v-2H0v2h2zm2 2H2v-2h2v2zm4 2H4v-2h4v2zm8 0v2H8v-2h8zm4-2v2h-4v-2h4zm2-2v2h-2v-2h2zm0-2h2v2h-2v-2zm-2-2h2v2h-2v-2zm0 0V8h-4v2h4zm-10 1h4v4h-4v-4z" fill="currentColor" /> </svg>} />
<Button2 title="/clear" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16 2v4h6v2h-2v14H4V8H2V6h6V2h8zm-2 2h-4v2h4V4zm0 4H6v12h12V8h-4z" fill="currentColor" /> </svg>} />
<Button2 title="/setspawnpoint" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M13 2v4h5v5h4v2h-4v5h-5v4h-2v-4H6v-5H2v-2h4V6h5V2h2zM8 8v8h8V8H8zm2 2h4v4h-4v-4z" fill="currentColor" /> </svg>} />
</div>
}
const meta: Meta<any> = {
component: Comp,
}
export default meta
type Story = StoryObj<any>;
export const Primary: Story = {
args: {
},
}

View file

@ -27,26 +27,29 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
}, [])
return <Screen title="Create world" backdrop="dirt">
<div style={{ display: 'flex' }}>
<form style={{ display: 'flex' }} onSubmit={(e) => {
e.preventDefault()
createClick()
}}>
<Input
autoFocus
value={title}
onChange={({ target: { value } }) => {
creatingWorldState.title = value
}}
onEnterPress={() => {
createClick()
}}
placeholder='World name'
/>
<select value={version} onChange={({ target: { value } }) => {
<select value={version} style={{
background: 'gray',
color: 'white'
}} onChange={({ target: { value } }) => {
creatingWorldState.version = value
}}>
{versions.map(({ version, label }) => {
return <option key={version} value={version}>{label}</option>
})}
</select>
</div>
</form>
<div style={{ display: 'flex' }}>
<Button onClick={() => {
const index = worldTypes.indexOf(type)

View file

@ -1,8 +1,8 @@
import { useEffect } from 'react'
import { proxy, useSnapshot } from 'valtio'
import { disconnect } from '../utils'
import { disconnect } from '../flyingSquidUtils'
import { MessageFormatPart, formatMessage } from '../botUtils'
import { showModal, hideModal, activeModalStack } from '../globalState'
import { showModal, hideModal } from '../globalState'
import { options } from '../optionsStorage'
import DeathScreen from './DeathScreen'
import { useIsModalActive } from './utils'

View file

@ -1,6 +1,6 @@
import { useUsingTouch } from '@dimaka/interface'
import { useEffect, useState } from 'react'
import Button from './Button'
import { useUsingTouch } from './utils'
export default () => {
const [fullScreen, setFullScreen] = useState(false)

View file

@ -1,21 +1,17 @@
import React, { useEffect, useRef } from 'react'
import styles from './input.module.css'
import { useUsingTouch } from './utils'
interface Props extends React.ComponentProps<'input'> {
autoFocus?: boolean
onEnterPress?: (e) => void
}
export default ({ autoFocus, onEnterPress, ...inputProps }: Props) => {
export default ({ autoFocus, ...inputProps }: Props) => {
const ref = useRef<HTMLInputElement>(null!)
const isTouch = useUsingTouch()
useEffect(() => {
if (onEnterPress) {
ref.current.addEventListener('keydown', (e) => {
if (e.code === 'Enter') onEnterPress(e)
})
}
if (!autoFocus || matchMedia('(pointer: coarse)').matches) return // Don't make screen keyboard popup on mobile
if (!autoFocus || isTouch) return // Don't make screen keyboard popup on mobile
ref.current.focus()
}, [])

View file

@ -4,8 +4,9 @@ import { useSnapshot } from 'valtio'
import { useEffect } from 'react'
import { activeModalStack, miscUiState, openOptionsMenu, showModal } from '../globalState'
import { openURL } from '../menus/components/common'
import { openFilePicker, openGithub, setLoadingScreenStatus } from '../utils'
import { copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs'
import { openGithub, setLoadingScreenStatus } from '../utils'
import { openFilePicker, copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs'
import MainMenu from './MainMenu'
// todo clean

View file

@ -0,0 +1,7 @@
import { CSSProperties } from 'react'
// names: https://pixelarticons.com/free/
export default ({ iconName, width = undefined as undefined | number, styles = {} as CSSProperties, className = undefined }) => {
if (width !== undefined) styles = { width, height: width, ...styles }
return <iconify-icon icon={`pixelarticons:${iconName}`} style={styles} className={className} />
}

View file

@ -0,0 +1,219 @@
import { CSSProperties, PointerEvent, PointerEventHandler, useEffect, useRef, useState } from 'react'
import { proxy, ref, useSnapshot } from 'valtio'
import { contro } from '../controls'
import worldInteractions from '../worldInteractions'
import PixelartIcon from './PixelartIcon'
import Button from './Button'
export type ButtonName = 'action' | 'sneak' | 'break'
type ButtonsPositions = Record<ButtonName, [number, number]>
interface Props {
touchActive: boolean
setupActive: boolean
buttonsPositions: ButtonsPositions
closeButtonsSetup: (newPositions?: ButtonsPositions) => void
}
const getCurrentAppScaling = () => {
// body has css property --guiScale
const guiScale = getComputedStyle(document.body).getPropertyValue('--guiScale')
return parseFloat(guiScale)
}
export const joystickPointer = proxy({
pointer: null as { x: number, y: number, pointerId: number } | null,
joystickInner: null as HTMLDivElement | null,
})
export const handleMovementStickDelta = (e?: { clientX, clientY }) => {
const max = 32
let x = 0
let y = 0
if (e) {
const scale = getCurrentAppScaling()
x = e.clientX - joystickPointer.pointer!.x
y = e.clientY - joystickPointer.pointer!.y
x = Math.min(Math.max(x, -max), max) / scale
y = Math.min(Math.max(y, -max), max) / scale
}
joystickPointer.joystickInner!.style.transform = `translate(${x}px, ${y}px)`
void contro.emit('movementUpdate', {
vector: {
x: x / max,
y: 0,
z: y / max,
},
})
}
export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup }: Props) => {
if (setupActive) touchActive = true
const joystickOuter = useRef<HTMLDivElement>(null)
const joystickInner = useRef<HTMLDivElement>(null)
const { pointer } = useSnapshot(joystickPointer)
const newButtonPositions = { ...buttonsPositions }
const buttonProps = (name: ButtonName) => {
let active = {
action: false,
sneak: bot.getControlState('sneak'),
break: false
}[name]
const holdDown = {
action () {
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
worldInteractions.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
},
sneak () {
bot.setControlState('sneak', !bot.getControlState('sneak'))
active = bot.getControlState('sneak')
},
break () {
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
worldInteractions.update()
active = true
}
}
const holdUp = {
action () {
},
sneak () {
},
break () {
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
worldInteractions.update()
active = false
}
}
type PType = PointerEvent<HTMLDivElement>
const pointerup = (e: PType) => {
const elem = e.currentTarget as HTMLElement
console.log(e.type, elem.hasPointerCapture(e.pointerId))
elem.releasePointerCapture(e.pointerId)
if (!setupActive) {
holdUp[name]()
pointerToggledUpdate(e)
}
}
const pointerToggledUpdate = (e) => {
e.currentTarget.style.background = active ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)'
}
let setupPointer = null as { x, y } | null
return {
style: {
position: 'fixed',
left: `${buttonsPositions[name][0]}%`,
top: `${buttonsPositions[name][1]}%`,
borderRadius: '50%',
width: '32px',
height: '32px',
background: active ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transition: 'background 0.1s',
} satisfies CSSProperties,
onPointerDown (e: PType) {
const elem = e.currentTarget as HTMLElement
elem.setPointerCapture(e.pointerId)
if (setupActive) {
setupPointer = { x: e.clientX, y: e.clientY }
} else {
holdDown[name]()
pointerToggledUpdate(e)
}
},
onPointerMove (e: PType) {
if (setupPointer) {
const elem = e.currentTarget as HTMLElement
const size = 32
const scale = getCurrentAppScaling()
const xPerc = e.clientX / window.innerWidth * 100 - size / scale
const yPerc = e.clientY / window.innerHeight * 100 - size / scale
elem.style.left = `${xPerc}%`
elem.style.top = `${yPerc}%`
newButtonPositions[name] = [xPerc, yPerc]
}
},
onPointerUp: pointerup,
// onPointerCancel: pointerup,
onLostPointerCapture: pointerup,
}
}
useEffect(() => {
joystickPointer.joystickInner = joystickInner.current && ref(joystickInner.current)
}, [])
if (!touchActive) return null
return <div>
<div
className='movement_joystick_outer'
ref={joystickOuter}
style={{
display: pointer ? 'flex' : 'none',
borderRadius: '50%',
width: 50,
height: 50,
border: '2px solid rgba(0, 0, 0, 0.5)',
backgroundColor: 'rgba(255, 255, div, 0.5)',
position: 'fixed',
justifyContent: 'center',
alignItems: 'center',
translate: '-50% -50%',
...pointer ? {
left: `${pointer.x / window.innerWidth * 100}%`,
top: `${pointer.y / window.innerHeight * 100}%`
} : {}
}}>
<div
className='movement_joystick_inner'
style={{
borderRadius: '50%',
width: 20,
height: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
position: 'absolute',
}}
ref={joystickInner}
/>
</div>
<div {...buttonProps('action')}>
<PixelartIcon iconName='circle' />
</div>
<div {...buttonProps('sneak')}>
<PixelartIcon iconName='arrow-down' />
</div>
<div {...buttonProps('break')}>
<MineIcon />
</div>
{setupActive && <div style={{
position: 'fixed',
bottom: 0,
display: 'flex',
justifyContent: 'center',
gap: 3
}}>
<Button onClick={() => {
closeButtonsSetup()
}}>Cancel</Button>
<Button onClick={() => {
closeButtonsSetup(newButtonPositions)
}}>Apply</Button>
</div>}
</div>
}
const MineIcon = () => {
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" width={22} height={22}>
<path d="M 8 0 L 8 2 L 18 2 L 18 0 L 8 0 z M 18 2 L 18 4 L 20 4 L 20 6 L 22 6 L 22 8 L 24 8 L 24 2 L 22 2 L 18 2 z M 24 8 L 24 18 L 26 18 L 26 8 L 24 8 z M 24 18 L 22 18 L 22 20 L 24 20 L 24 18 z M 22 18 L 22 10 L 20 10 L 20 18 L 22 18 z M 20 10 L 20 8 L 18 8 L 18 10 L 20 10 z M 18 10 L 16 10 L 16 12 L 18 12 L 18 10 z M 16 12 L 14 12 L 14 14 L 16 14 L 16 12 z M 14 14 L 12 14 L 12 16 L 14 16 L 14 14 z M 12 16 L 10 16 L 10 18 L 12 18 L 12 16 z M 10 18 L 8 18 L 8 20 L 10 20 L 10 18 z M 8 20 L 6 20 L 6 22 L 8 22 L 8 20 z M 6 22 L 4 22 L 4 24 L 6 24 L 6 22 z M 4 24 L 2 24 L 2 22 L 0 22 L 0 24 L 0 26 L 2 26 L 4 26 L 4 24 z M 2 22 L 4 22 L 4 20 L 2 20 L 2 22 z M 4 20 L 6 20 L 6 18 L 4 18 L 4 20 z M 6 18 L 8 18 L 8 16 L 6 16 L 6 18 z M 8 16 L 10 16 L 10 14 L 8 14 L 8 16 z M 10 14 L 12 14 L 12 12 L 10 12 L 10 14 z M 12 12 L 14 12 L 14 10 L 12 10 L 12 12 z M 14 10 L 16 10 L 16 8 L 14 8 L 14 10 z M 16 8 L 18 8 L 18 6 L 16 6 L 16 8 z M 16 6 L 16 4 L 8 4 L 8 6 L 16 6 z M 8 4 L 8 2 L 6 2 L 6 4 L 8 4 z" stroke='white' />
</svg>
}

View file

@ -0,0 +1,19 @@
import { useSnapshot } from 'valtio'
import { activeModalStack, hideModal } from '../globalState'
import { options } from '../optionsStorage'
import TouchAreasControls from './TouchAreasControls'
import { useIsModalActive, useUsingTouch } from './utils'
export default () => {
const usingTouch = useUsingTouch()
const hasModals = useSnapshot(activeModalStack).length !== 0
const setupActive = useIsModalActive('touch-buttons-setup')
const { touchControlsPositions, touchControlsType } = useSnapshot(options)
return <TouchAreasControls touchActive={!!usingTouch && !hasModals && touchControlsType === 'joystick-buttons'} setupActive={setupActive} buttonsPositions={touchControlsPositions as any} closeButtonsSetup={(newPositions) => {
if (newPositions) {
options.touchControlsPositions = newPositions
}
hideModal()
}} />
}

View file

@ -1,9 +1,10 @@
import { LeftTouchArea, RightTouchArea, useInterfaceState, useUsingTouch } from '@dimaka/interface'
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
import { css } from '@emotion/css'
import { useSnapshot } from 'valtio'
import { contro } from '../controls'
import { miscUiState, activeModalStack } from '../globalState'
import { watchValue, options } from '../optionsStorage'
import { useUsingTouch } from './utils'
// todo
useInterfaceState.setState({
@ -48,8 +49,9 @@ export default () => {
const usingTouch = useUsingTouch()
const { usingGamepadInput } = useSnapshot(miscUiState)
const modals = useSnapshot(activeModalStack)
const { touchControlsType } = useSnapshot(options)
if (!usingTouch || usingGamepadInput) return null
if (!usingTouch || usingGamepadInput || touchControlsType !== 'classic') return null
return (
<div
style={{ zIndex: modals.length ? 7 : 8 }}

View file

@ -14,7 +14,8 @@
flex: 1;
margin: 5px;
overflow: auto;
scrollbar-width: thin;
/* todo think of better workaround */
scrollbar-width: var(--thin-if-firefox);
}
.world_root {

View file

@ -1,6 +1,7 @@
import { useSnapshot } from 'valtio'
import { useEffect, useRef } from 'react'
import { activeModalStack } from '../globalState'
import { UAParser } from 'ua-parser-js'
import { activeModalStack, miscUiState } from '../globalState'
export const useIsModalActive = (modal: string) => {
return useSnapshot(activeModalStack).at(-1)?.reactType === modal
@ -25,3 +26,11 @@ export function useDidUpdateEffect (fn, inputs) {
}
}, inputs)
}
export const useUsingTouch = () => {
return useSnapshot(miscUiState).currentTouch
}
export const ua = new UAParser(navigator.userAgent)
export const isIos = ua.getOS().name === 'iOS'

View file

@ -18,6 +18,8 @@ import SoundMuffler from './react/SoundMuffler'
import TouchControls from './react/TouchControls'
import widgets from './react/widgets'
import { useIsWidgetActive } from './react/utils'
import GlobalSearchInput from './GlobalSearchInput'
import TouchAreasControlsProvider from './react/TouchAreasControlsProvider'
const Portal = ({ children, to }) => {
return createPortal(children, to)
@ -58,11 +60,13 @@ const InGameUi = () => {
<DeathScreenProvider />
<ChatProvider />
<SoundMuffler />
<TouchAreasControlsProvider />
</Portal>
<DisplayQr />
<Portal to={document.body}>
{/* becaues of z-index */}
<TouchControls />
<GlobalSearchInput />
</Portal>
</>
}

View file

@ -15,6 +15,7 @@
.backdrop {
position: fixed;
inset: 0;
height: 100dvh;
background: rgba(0, 0, 0, 0.75);
}

View file

@ -51,6 +51,7 @@ body {
-ms-user-select: none;
user-select: none;
font-family: minecraft, mojangles, monospace;
z-index: -5;
}
#react-root {
@ -148,10 +149,21 @@ body {
animation-fill-mode: forwards;
}
.full-svg svg {
width: 100%;
height: 100%;
}
.muted {
color: #999;
}
@media screen and (min-width: 430px) {
.span-2 {
grid-column: span 2;
}
}
@keyframes dive-animation {
0% {
transform: translateZ(-150px);

View file

@ -76,7 +76,7 @@ export const installTexturePack = async (file: File | ArrayBuffer, name = file['
const allFilesArr = Object.entries(zipFile.files)
let done = 0
const upStatus = () => {
setLoadingScreenStatus(`${status} ${Math.round(++done / allFilesArr.length * 100)}%`)
setLoadingScreenStatus(`${status} ${Math.round(done / allFilesArr.length * 100)}%`)
}
await Promise.all(allFilesArr.map(async ([path, file]) => {
const writePath = join(texturePackBasePath, path)

View file

@ -18,6 +18,7 @@ const addStat = (dom, size = 80) => {
if (denseMode) dom.style.height = '12px'
dom.style.overflow = 'hidden'
dom.style.left = ''
dom.style.top = 0
dom.style.right = `${total}px`
dom.style.width = '80px'
dom.style.zIndex = 1000
@ -45,7 +46,7 @@ if (hideStats) {
export const initWithRenderer = (canvas) => {
if (hideStats) return
statsGl.init(canvas)
if (statsGl.gpuPanel) {
if (statsGl.gpuPanel && process.env.NODE_ENV !== 'production') {
addStatsGlStat(statsGl.gpuPanel.canvas)
}
// addStatsGlStat(statsGl.msPanel.canvas)

View file

@ -1,9 +1,6 @@
import { hideModal, isGameActive, miscUiState, notification, showModal } from './globalState'
import { options } from './optionsStorage'
import { openWorldZip } from './browserfs'
import { installTexturePack } from './texturePack'
import { appStatusState } from './react/AppStatusProvider'
import { saveServer } from './flyingSquidUtils'
export const goFullscreen = async (doToggle = false) => {
if (!document.fullscreenElement) {
@ -147,17 +144,6 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
appStatusState.status = status
}
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
bot.end('You left the server')
}
// doesn't support snapshots
export const toMajorVersion = (version) => {
const [a, b] = (String(version)).split('.')
@ -181,35 +167,6 @@ export const reloadChunks = async () => {
await worldView.updatePosition(bot.entity.position, true)
}
export const openFilePicker = (specificCase?: 'resourcepack') => {
// create and show input picker
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
if (!picker) {
picker = document.createElement('input')
picker.type = 'file'
picker.accept = '.zip'
picker.addEventListener('change', () => {
const file = picker.files?.[0]
picker.value = ''
if (!file) return
if (!file.name.endsWith('.zip')) {
const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? Only .zip files are supported. Continue?`)
if (!doContinue) return
}
if (specificCase === 'resourcepack') {
void installTexturePack(file)
} else {
void openWorldZip(file)
}
})
picker.hidden = true
document.body.appendChild(picker)
}
picker.click()
}
export const openGithub = () => {
window.open(process.env.GITHUB_URL, '_blank')
}
@ -230,3 +187,11 @@ export function assertDefined<T> (x: T | undefined): asserts x is T {
export const haveDirectoryPicker = () => {
return !!window.showDirectoryPicker
}
const reportedWarnings = new Set<string>()
export const reportWarningOnce = (id: string, message: string) => {
if (reportedWarnings.has(id)) return
reportedWarnings.add(id)
console.warn(message)
}

View file

@ -3,6 +3,8 @@
import { subscribeKey } from 'valtio/utils'
import { options, watchValue } from './optionsStorage'
import { reloadChunks } from './utils'
import { miscUiState } from './globalState'
import { isMobile } from './menus/components/common'
subscribeKey(options, 'renderDistance', reloadChunks)
subscribeKey(options, 'multiplayerRenderDistance', reloadChunks)
@ -14,7 +16,17 @@ watchValue(options, o => {
document.documentElement.style.setProperty('--guiScale', `${o.guiScale}`)
})
/** happens once */
export const watchOptionsAfterViewerInit = () => {
const updateTouch = (o) => {
miscUiState.currentTouch = o.alwaysShowMobileControls || isMobile()
}
watchValue(options, updateTouch)
window.matchMedia('(pointer: coarse)').addEventListener('change', (e) => {
updateTouch(options)
})
watchValue(options, o => {
if (!viewer) return
viewer.world.showChunkBorders = o.showChunkBorders