Merge remote-tracking branch 'origin/develop' into webgpu
This commit is contained in:
commit
ac07b5cfbc
68 changed files with 4118 additions and 530 deletions
1
.github/workflows/preview.yml
vendored
1
.github/workflows/preview.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
16
README.MD
16
README.MD
|
|
@ -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
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
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
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
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
BIN
assets/generic_95.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -3,5 +3,5 @@
|
|||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "zardoy.site:2344",
|
||||
"defaultVersion": "1.18.2",
|
||||
"mapsProvider": "zardoy.site/maps"
|
||||
"mapsProvider": "https://maps.mcraft.fun/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
75
index.html
75
index.html
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
596
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
emitEntity(e)
|
||||
},
|
||||
entityUpdate: (e: any) => {
|
||||
emitEntity(e)
|
||||
},
|
||||
entityMoved: (e: any) => {
|
||||
emitEntity(e)
|
||||
|
|
|
|||
|
|
@ -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
34
src/GlobalSearchInput.tsx
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const commands: Array<{
|
|||
{
|
||||
command: ['/save'],
|
||||
async invoke () {
|
||||
await saveServer()
|
||||
await saveServer(false)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
17
src/gameUnload.ts
Normal 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 = []
|
||||
}
|
||||
})
|
||||
543
src/generatedClientPackets.ts
Normal file
543
src/generatedClientPackets.ts
Normal 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
|
||||
1495
src/generatedServerPackets.ts
Normal file
1495
src/generatedServerPackets.ts
Normal file
File diff suppressed because it is too large
Load diff
35
src/globalDomListeners.ts
Normal file
35
src/globalDomListeners.ts
Normal 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?'
|
||||
})
|
||||
|
|
@ -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
16
src/globals.d.ts
vendored
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ??= () => []
|
||||
}
|
||||
|
|
|
|||
93
src/index.ts
93
src/index.ts
|
|
@ -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--
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ type Story = StoryObj<typeof Chat>
|
|||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
usingTouch: false,
|
||||
messages: [{
|
||||
parts: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
52
src/react/ConceptCommandsGui.stories.tsx
Normal file
52
src/react/ConceptCommandsGui.stories.tsx
Normal 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: {
|
||||
},
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
src/react/PixelartIcon.tsx
Normal file
7
src/react/PixelartIcon.tsx
Normal 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} />
|
||||
}
|
||||
219
src/react/TouchAreasControls.tsx
Normal file
219
src/react/TouchAreasControls.tsx
Normal 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>
|
||||
}
|
||||
19
src/react/TouchAreasControlsProvider.tsx
Normal file
19
src/react/TouchAreasControlsProvider.tsx
Normal 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()
|
||||
}} />
|
||||
}
|
||||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
height: 100dvh;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
51
src/utils.ts
51
src/utils.ts
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue