Merge remote-tracking branch 'origin/next' into renderer-rewrite
This commit is contained in:
commit
d74d860726
34 changed files with 609 additions and 130 deletions
43
.github/workflows/build-zip.yml
vendored
Normal file
43
.github/workflows/build-zip.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: Make Self Host Zip
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Bundle server.js
|
||||
run: |
|
||||
pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'"
|
||||
|
||||
- name: Create distribution package
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cp bundled-server.js package/server.js
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: self-host
|
||||
path: self-host.zip
|
||||
2
.github/workflows/next-deploy.yml
vendored
2
.github/workflows/next-deploy.yml
vendored
|
|
@ -32,6 +32,8 @@ jobs:
|
|||
echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
|
|
@ -61,6 +61,8 @@ jobs:
|
|||
echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
|
|
|
|||
33
.github/workflows/publish.yml
vendored
33
.github/workflows/publish.yml
vendored
|
|
@ -30,6 +30,8 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
|
|
@ -43,19 +45,36 @@ jobs:
|
|||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# has possible output: tag
|
||||
id: release
|
||||
# has output
|
||||
# publish to github
|
||||
- run: cp vercel.json .vercel/output/static/vercel.json
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: .vercel/output/static
|
||||
force_orphan: true
|
||||
|
||||
- name: Build self-host version
|
||||
run: pnpm build
|
||||
- name: Bundle server.js
|
||||
run: |
|
||||
pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'"
|
||||
|
||||
- name: Create zip package
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cp bundled-server.js package/server.js
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# has possible output: tag
|
||||
id: release
|
||||
|
||||
# has output
|
||||
- name: Set publishing config
|
||||
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ RUN npm i -g pnpm@9.0.4
|
|||
# Build arguments
|
||||
ARG DOWNLOAD_SOUNDS=false
|
||||
ARG DISABLE_SERVICE_WORKER=false
|
||||
ARG CONFIG_JSON_SOURCE=REMOTE
|
||||
# TODO need flat --no-root-optional
|
||||
RUN node ./scripts/dockerPrepare.mjs
|
||||
RUN pnpm i
|
||||
|
|
@ -22,8 +23,8 @@ RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs
|
|||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||
|
||||
# only for prod
|
||||
RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \
|
||||
DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
||||
RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
||||
CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \
|
||||
pnpm run build
|
||||
|
||||
# ---- Run Stage ----
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
"peerJsServer": "",
|
||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "ws://mcraft.ryzyn.xyz",
|
||||
"version": "1.19.4"
|
||||
},
|
||||
{
|
||||
"ip": "ws://play.mcraft.fun"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@
|
|||
"web",
|
||||
"client"
|
||||
],
|
||||
"release": {
|
||||
"attachReleaseFiles": "self-host.zip"
|
||||
},
|
||||
"publish": {
|
||||
"preset": {
|
||||
"publishOnlyIfChanged": true,
|
||||
|
|
@ -145,8 +148,8 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.42",
|
||||
"mineflayer-mouse": "^0.0.5",
|
||||
"mc-assets": "^0.2.37",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
|
|||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
|
|
@ -350,8 +350,8 @@ importers:
|
|||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
mc-assets:
|
||||
specifier: ^0.2.37
|
||||
version: 0.2.37
|
||||
specifier: ^0.2.42
|
||||
version: 0.2.42
|
||||
minecraft-inventory-gui:
|
||||
specifier: github:zardoy/minecraft-inventory-gui#next
|
||||
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0)
|
||||
|
|
@ -3589,9 +3589,6 @@ packages:
|
|||
resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
apl-image-packer@1.1.0:
|
||||
resolution: {integrity: sha512-Pb1Q76cp8xpY8X4OqVrnk5v1/tB5kOtCzwgOnx8IxMNeekFh/eNUiUKeX5fvGNViZiLmuKAAQc5ICuBDspZ4cA==}
|
||||
|
||||
app-root-dir@1.0.2:
|
||||
resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==}
|
||||
|
||||
|
|
@ -6571,8 +6568,11 @@ packages:
|
|||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mc-assets@0.2.37:
|
||||
resolution: {integrity: sha512-49tk3shwxsDoV0PrrbORZEKg613vUQPULgusWjXNl8JEma5y41LEo57D6q4aHliXsV3Gb9ThrkFf6hIb0WlY1Q==}
|
||||
maxrects-packer@2.7.3:
|
||||
resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==}
|
||||
|
||||
mc-assets@0.2.42:
|
||||
resolution: {integrity: sha512-j2D1RNYtB5Z9gFu9MVjyDBbiALI0mWZ3xW/A3PPefVAHm3HJ2T1vH+1XBov1spBGPl7u+Zo7mRXza3X0egbeOg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
mcraft-fun-mineflayer@0.1.8:
|
||||
|
|
@ -13609,8 +13609,6 @@ snapshots:
|
|||
|
||||
apache-md5@1.1.8: {}
|
||||
|
||||
apl-image-packer@1.1.0: {}
|
||||
|
||||
app-root-dir@1.0.2: {}
|
||||
|
||||
aproba@2.0.0:
|
||||
|
|
@ -17367,9 +17365,11 @@ snapshots:
|
|||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mc-assets@0.2.37:
|
||||
maxrects-packer@2.7.3: {}
|
||||
|
||||
mc-assets@0.2.42:
|
||||
dependencies:
|
||||
apl-image-packer: 1.1.0
|
||||
maxrects-packer: 2.7.3
|
||||
zod: 3.24.1
|
||||
|
||||
mcraft-fun-mineflayer@0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)):
|
||||
|
|
|
|||
275
renderer/viewer/lib/guiRenderer.ts
Normal file
275
renderer/viewer/lib/guiRenderer.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
// Import placeholders - replace with actual imports for your environment
|
||||
import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate'
|
||||
import { mat4, vec3 } from 'gl-matrix'
|
||||
import { AssetsParser } from 'mc-assets/dist/assetsParser'
|
||||
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||
import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets'
|
||||
import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores'
|
||||
import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
|
||||
import { proxy, ref } from 'valtio'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
|
||||
export const activeGuiAtlas = proxy({
|
||||
atlas: null as null | { json, image },
|
||||
})
|
||||
|
||||
export const getNonFullBlocksModels = () => {
|
||||
const version = viewer.world.texturesVersion ?? 'latest'
|
||||
const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest
|
||||
const blockModelsResolved = {} as Record<string, any>
|
||||
const itemsModelsResolved = {} as Record<string, any>
|
||||
const fullBlocksWithNonStandardDisplay = [] as string[]
|
||||
const handledItemsWithDefinitions = new Set()
|
||||
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels))
|
||||
|
||||
const standardGuiDisplay = {
|
||||
'rotation': [
|
||||
30,
|
||||
225,
|
||||
0
|
||||
],
|
||||
'translation': [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
'scale': [
|
||||
0.625,
|
||||
0.625,
|
||||
0.625
|
||||
]
|
||||
}
|
||||
|
||||
const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i])
|
||||
const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => {
|
||||
if (blockModelsResolved[name]) return
|
||||
if (!model?.elements?.length) return
|
||||
const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16])
|
||||
if (isFullBlock) return
|
||||
model['display'] ??= {}
|
||||
model['display']['gui'] ??= standardGuiDisplay
|
||||
blockModelsResolved[name] = model
|
||||
}
|
||||
|
||||
for (const [name, definition] of Object.entries(itemsDefinitions)) {
|
||||
const item = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
version,
|
||||
name,
|
||||
properties: {
|
||||
'minecraft:display_context': 'gui',
|
||||
},
|
||||
})
|
||||
if (item) {
|
||||
const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {}
|
||||
if (resolvedModel) {
|
||||
handledItemsWithDefinitions.add(name)
|
||||
}
|
||||
if (resolvedModel?.elements) {
|
||||
|
||||
let hasStandardDisplay = true
|
||||
if (resolvedModel['display']?.gui) {
|
||||
hasStandardDisplay =
|
||||
arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation)
|
||||
&& arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation)
|
||||
&& arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale)
|
||||
}
|
||||
|
||||
addModelIfNotFullblock(name, resolvedModel)
|
||||
|
||||
if (!blockModelsResolved[name] && !hasStandardDisplay) {
|
||||
fullBlocksWithNonStandardDisplay.push(name)
|
||||
}
|
||||
const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side'
|
||||
if (!hasStandardDisplay || notSideLight) {
|
||||
blockModelsResolved[name] = resolvedModel
|
||||
}
|
||||
}
|
||||
if (!blockModelsResolved[name] && item.tints && resolvedModel) {
|
||||
resolvedModel['tints'] = item.tints
|
||||
if (resolvedModel.elements) {
|
||||
blockModelsResolved[name] = resolvedModel
|
||||
} else {
|
||||
itemsModelsResolved[name] = resolvedModel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) {
|
||||
if (handledItemsWithDefinitions.has(name)) {
|
||||
continue
|
||||
}
|
||||
const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true)
|
||||
if (resolvedModel) {
|
||||
addModelIfNotFullblock(name, resolvedModel[0])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
blockModelsResolved,
|
||||
itemsModelsResolved
|
||||
}
|
||||
}
|
||||
|
||||
// customEvents.on('gameLoaded', () => {
|
||||
// const res = getNonFullBlocksModels()
|
||||
// })
|
||||
|
||||
const RENDER_SIZE = 64
|
||||
|
||||
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
|
||||
const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage)
|
||||
const canvasTemp = document.createElement('canvas')
|
||||
canvasTemp.width = img.width
|
||||
canvasTemp.height = img.height
|
||||
canvasTemp.style.imageRendering = 'pixelated'
|
||||
const ctx = canvasTemp.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.drawImage(img, 0, 0)
|
||||
|
||||
const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser!
|
||||
const textureAtlas = new TextureAtlas(
|
||||
ctx.getImageData(0, 0, img.width, img.height),
|
||||
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
|
||||
return [key, [
|
||||
value.u,
|
||||
value.v,
|
||||
(value.u + (value.su ?? atlasParser.atlas.latest.suSv)),
|
||||
(value.v + (value.sv ?? atlasParser.atlas.latest.suSv)),
|
||||
]] as [string, [number, number, number, number]]
|
||||
}))
|
||||
)
|
||||
|
||||
const PREVIEW_ID = Identifier.parse('preview:preview')
|
||||
const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined)
|
||||
|
||||
let modelData: any
|
||||
let currentModelName: string | undefined
|
||||
const resources: ItemRendererResources = {
|
||||
getBlockModel (id) {
|
||||
if (id.equals(PREVIEW_ID)) {
|
||||
return BlockModel.fromJson(modelData ?? {})
|
||||
}
|
||||
return null
|
||||
},
|
||||
getTextureUV (texture) {
|
||||
return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any)
|
||||
},
|
||||
getTextureAtlas () {
|
||||
return textureAtlas.getTextureAtlas()
|
||||
},
|
||||
getItemComponents (id) {
|
||||
return new Map()
|
||||
},
|
||||
getItemModel (id) {
|
||||
// const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident'
|
||||
const isSpecial = false
|
||||
if (id.equals(PREVIEW_ID)) {
|
||||
return ItemModel.fromJson({
|
||||
type: isSpecial ? 'minecraft:special' : 'minecraft:model',
|
||||
model: isSpecial ? {
|
||||
type: currentModelName,
|
||||
} : PREVIEW_ID.toString(),
|
||||
base: PREVIEW_ID.toString(),
|
||||
tints: modelData?.tints,
|
||||
})
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = RENDER_SIZE
|
||||
canvas.height = RENDER_SIZE
|
||||
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
|
||||
if (!gl) {
|
||||
throw new Error('Cannot get WebGL2 context')
|
||||
}
|
||||
|
||||
function resetGLContext (gl) {
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
}
|
||||
|
||||
// const includeOnly = ['powered_repeater', 'wooden_door']
|
||||
const includeOnly = [] as string[]
|
||||
|
||||
const images: Record<string, HTMLImageElement> = {}
|
||||
const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({
|
||||
'minecraft:item_model': new NbtString(PREVIEW_ID.toString()),
|
||||
})))
|
||||
const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' })
|
||||
const missingTextures = new Set()
|
||||
for (const [modelName, model] of Object.entries(models)) {
|
||||
if (includeOnly.length && !includeOnly.includes(modelName)) continue
|
||||
|
||||
const patchMissingTextures = () => {
|
||||
for (const element of model.elements ?? []) {
|
||||
for (const [faceName, face] of Object.entries(element.faces)) {
|
||||
if (face.texture.startsWith('#')) {
|
||||
missingTextures.add(`${modelName} ${faceName}: ${face.texture}`)
|
||||
face.texture = 'block/unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
patchMissingTextures()
|
||||
// TODO eggs
|
||||
|
||||
modelData = model
|
||||
currentModelName = modelName
|
||||
resetGLContext(gl)
|
||||
if (!modelData) continue
|
||||
renderer.setItem(item, { display_context: 'gui' })
|
||||
renderer.drawItem()
|
||||
const url = canvas.toDataURL()
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const img = await getLoadedImage(url)
|
||||
images[modelName] = img
|
||||
}
|
||||
|
||||
if (missingTextures.size) {
|
||||
console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`)
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
|
||||
const atlas = makeTextureAtlas({
|
||||
input: Object.keys(images),
|
||||
tileSize: RENDER_SIZE,
|
||||
getLoadedImage (name) {
|
||||
return {
|
||||
image: images[name],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL())
|
||||
// const a = document.createElement('a')
|
||||
// a.href = await atlasParser.createDebugImage(true)
|
||||
// a.download = 'blocks_atlas.png'
|
||||
// a.click()
|
||||
|
||||
activeGuiAtlas.atlas = {
|
||||
json: atlas.json,
|
||||
image: ref(await getLoadedImage(atlas.canvas.toDataURL())),
|
||||
}
|
||||
|
||||
return atlas
|
||||
}
|
||||
|
||||
export const generateGuiAtlas = async () => {
|
||||
const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels()
|
||||
|
||||
// Generate blocks atlas
|
||||
console.time('generate blocks gui atlas')
|
||||
const blockImages = await generateItemsGui(blockModelsResolved, false)
|
||||
console.timeEnd('generate blocks gui atlas')
|
||||
console.time('generate items gui atlas')
|
||||
const itemImages = await generateItemsGui(itemsModelsResolved, true)
|
||||
console.timeEnd('generate items gui atlas')
|
||||
await generateAtlas({ ...blockImages, ...itemImages })
|
||||
// await generateAtlas(blockImages)
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { buildCleanupDecorator } from './cleanupDecorator'
|
|||
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { updateStatText } from './ui/newStats'
|
||||
import { generateGuiAtlas } from './guiRenderer'
|
||||
|
||||
const appViewer = undefined
|
||||
|
||||
|
|
@ -332,6 +333,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
async generateGuiTextures () {
|
||||
await generateGuiAtlas()
|
||||
}
|
||||
|
||||
async updateAssetsData () {
|
||||
const texture = await new THREE.TextureLoader().loadAsync(this.resourcesManager.blocksAtlasParser!.latestImage)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
|
|
@ -365,11 +370,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
config: this.mesherConfig,
|
||||
})
|
||||
}
|
||||
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.resourcesManager.itemsAtlasParser!.latestImage)
|
||||
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage)
|
||||
itemsTexture.magFilter = THREE.NearestFilter
|
||||
itemsTexture.minFilter = THREE.NearestFilter
|
||||
itemsTexture.flipY = false
|
||||
viewer.entities.itemsTexture = itemsTexture
|
||||
|
||||
this.renderUpdateEmitter.emit('textureDownloaded')
|
||||
this.renderUpdateEmitter.emit('itemsTextureDownloaded')
|
||||
console.log('textures loaded')
|
||||
}
|
||||
|
||||
async downloadDebugAtlas (isItems = false) {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true'
|
|||
let releaseTag
|
||||
let releaseLink
|
||||
let releaseChangelog
|
||||
let githubRepositoryFallback
|
||||
|
||||
if (fs.existsSync('./assets/release.json')) {
|
||||
const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8'))
|
||||
releaseTag = releaseJson.latestTag
|
||||
releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}`
|
||||
releaseChangelog = releaseJson.changelog?.replace(/<!-- bump-type:[\w]+ -->/, '')
|
||||
githubRepositoryFallback = releaseJson.repository
|
||||
}
|
||||
|
||||
const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
|
||||
|
|
@ -41,6 +43,8 @@ if (dev) {
|
|||
configJson.defaultProxy = ':8080'
|
||||
}
|
||||
|
||||
const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE'
|
||||
|
||||
// base options are in ./renderer/rsbuildSharedConfig.ts
|
||||
const appConfig = defineConfig({
|
||||
html: {
|
||||
|
|
@ -66,13 +70,13 @@ const appConfig = defineConfig({
|
|||
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
|
||||
'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS),
|
||||
'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}` || githubRepositoryFallback}`),
|
||||
'process.env.DEPS_VERSIONS': JSON.stringify({}),
|
||||
'process.env.RELEASE_TAG': JSON.stringify(releaseTag),
|
||||
'process.env.RELEASE_LINK': JSON.stringify(releaseLink),
|
||||
'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog),
|
||||
'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker),
|
||||
'process.env.INLINED_APP_CONFIG': JSON.stringify(configJson),
|
||||
'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
|
@ -109,7 +113,9 @@ const appConfig = defineConfig({
|
|||
fs.copyFileSync('./assets/release.json', './dist/release.json')
|
||||
}
|
||||
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
|
||||
if (configSource === 'REMOTE') {
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
|
||||
}
|
||||
if (fs.existsSync('./generated/sounds.js')) {
|
||||
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,27 @@ import path from 'path'
|
|||
import { fileURLToPath } from 'url'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
// write release tag
|
||||
// Get repository from git config
|
||||
const getGitRepository = () => {
|
||||
try {
|
||||
const gitConfig = fs.readFileSync('.git/config', 'utf8')
|
||||
const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m)
|
||||
if (originUrlMatch) {
|
||||
return originUrlMatch[1]
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to read git repository from config:', err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// write release tag and repository info
|
||||
const commitShort = execSync('git rev-parse --short HEAD').toString().trim()
|
||||
fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8')
|
||||
const repository = getGitRepository()
|
||||
fs.writeFileSync('./assets/release.json', JSON.stringify({
|
||||
latestTag: `${commitShort} (docker)`,
|
||||
repository
|
||||
}), 'utf8')
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
|
||||
delete packageJson.optionalDependencies
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ try {
|
|||
// Create our app
|
||||
const app = express()
|
||||
|
||||
const isProd = process.argv.includes('--prod')
|
||||
const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production'
|
||||
app.use(compression())
|
||||
app.use(cors())
|
||||
app.use(netApi({ allowOrigin: '*' }))
|
||||
|
|
|
|||
59
src/appConfig.ts
Normal file
59
src/appConfig.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { disabledSettings, options, qsOptions } from './optionsStorage'
|
||||
import { miscUiState } from './globalState'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
|
||||
export type AppConfig = {
|
||||
// defaultHost?: string
|
||||
// defaultHostSave?: string
|
||||
defaultProxy?: string
|
||||
// defaultProxySave?: string
|
||||
// defaultVersion?: string
|
||||
peerJsServer?: string
|
||||
peerJsServerFallback?: string
|
||||
promoteServers?: Array<{ ip, description, version? }>
|
||||
mapsProvider?: string
|
||||
|
||||
appParams?: Record<string, any> // query string params
|
||||
|
||||
defaultSettings?: Record<string, any>
|
||||
forceSettings?: Record<string, boolean>
|
||||
// hideSettings?: Record<string, boolean>
|
||||
allowAutoConnect?: boolean
|
||||
pauseLinks?: Array<Array<Record<string, any>>>
|
||||
}
|
||||
|
||||
export const loadAppConfig = (appConfig: AppConfig) => {
|
||||
if (miscUiState.appConfig) {
|
||||
Object.assign(miscUiState.appConfig, appConfig)
|
||||
} else {
|
||||
miscUiState.appConfig = appConfig
|
||||
}
|
||||
|
||||
if (appConfig.forceSettings) {
|
||||
for (const [key, value] of Object.entries(appConfig.forceSettings)) {
|
||||
if (value) {
|
||||
disabledSettings.value.add(key)
|
||||
// since the setting is forced, we need to set it to that value
|
||||
if (appConfig.defaultSettings?.[key] && !qsOptions[key]) {
|
||||
options[key] = appConfig.defaultSettings[key]
|
||||
}
|
||||
} else {
|
||||
disabledSettings.value.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG
|
||||
|
||||
if (isBundledConfigUsed) {
|
||||
loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {})
|
||||
} else {
|
||||
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
|
||||
// console.warn('Failed to load optional app config.json', error)
|
||||
// return {}
|
||||
setLoadingScreenStatus('Failed to load app config.json', true)
|
||||
}).then((config: AppConfig) => {
|
||||
loadAppConfig(config)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AppConfig } from './globalState'
|
||||
import type { AppConfig } from './appConfig'
|
||||
|
||||
const qsParams = new URLSearchParams(window.location?.search ?? '')
|
||||
|
||||
|
|
|
|||
|
|
@ -17,17 +17,30 @@ const convertedSounds = [] as string[]
|
|||
export async function loadSound (path: string, contents = path) {
|
||||
if (loadingSounds.includes(path)) return true
|
||||
loadingSounds.push(path)
|
||||
const res = await window.fetch(contents)
|
||||
if (!res.ok) {
|
||||
const error = `Failed to load sound ${path}`
|
||||
if (isCypress()) throw new Error(error)
|
||||
else console.warn(error)
|
||||
return
|
||||
}
|
||||
const data = await res.arrayBuffer()
|
||||
|
||||
sounds[path] = data
|
||||
loadingSounds.splice(loadingSounds.indexOf(path), 1)
|
||||
try {
|
||||
audioContext ??= new window.AudioContext()
|
||||
|
||||
const res = await window.fetch(contents)
|
||||
if (!res.ok) {
|
||||
const error = `Failed to load sound ${path}`
|
||||
if (isCypress()) throw new Error(error)
|
||||
else console.warn(error)
|
||||
return
|
||||
}
|
||||
const arrayBuffer = await res.arrayBuffer()
|
||||
|
||||
// Decode the audio data immediately
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
|
||||
sounds[path] = audioBuffer
|
||||
convertedSounds.push(path) // Mark as converted immediately
|
||||
|
||||
loadingSounds.splice(loadingSounds.indexOf(path), 1)
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load sound ${path}:`, err)
|
||||
loadingSounds.splice(loadingSounds.indexOf(path), 1)
|
||||
if (isCypress()) throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
|
||||
|
|
@ -53,13 +66,6 @@ export async function playSound (url, soundVolume = 1) {
|
|||
return
|
||||
}
|
||||
|
||||
for (const [soundName, sound] of Object.entries(sounds)) {
|
||||
if (convertedSounds.includes(soundName)) continue
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
sounds[soundName] = await audioContext.decodeAudioData(sound)
|
||||
convertedSounds.push(soundName)
|
||||
}
|
||||
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
console.warn(`Sound ${url} not loaded yet`)
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ browserfs.configure({
|
|||
throw e2
|
||||
}
|
||||
showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true)
|
||||
miscUiState.appLoaded = true
|
||||
miscUiState.fsReady = true
|
||||
miscUiState.singleplayerAvailable = false
|
||||
})
|
||||
return
|
||||
}
|
||||
await updateTexturePackInstalledState()
|
||||
miscUiState.appLoaded = true
|
||||
miscUiState.fsReady = true
|
||||
miscUiState.singleplayerAvailable = true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ export interface ProgressReporter {
|
|||
|
||||
setMessage (message: string): void
|
||||
|
||||
end (): void
|
||||
end(): void
|
||||
error(message: string): void
|
||||
}
|
||||
|
||||
interface ReporterDisplayImplementation {
|
||||
setMessage (message: string): void
|
||||
end (): void
|
||||
error(message: string): void
|
||||
}
|
||||
|
||||
interface StageInfo {
|
||||
|
|
@ -124,6 +126,10 @@ const createProgressReporter = (implementation: ReporterDisplayImplementation):
|
|||
|
||||
get currentMessage () {
|
||||
return currentMessage
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
implementation.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +151,11 @@ export const createFullScreenProgressReporter = (): ProgressReporter => {
|
|||
} else {
|
||||
setLoadingScreenStatus(fullScreenReporters.at(-1)!.currentMessage)
|
||||
}
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
if (appStatusState.isError) return
|
||||
setLoadingScreenStatus(message, true)
|
||||
}
|
||||
})
|
||||
fullScreenReporters.push(reporter)
|
||||
|
|
@ -162,6 +173,10 @@ export const createNotificationProgressReporter = (endMessage?: string): Progres
|
|||
} else {
|
||||
hideNotification()
|
||||
}
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
showNotification(message, '', true, '', undefined, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -173,6 +188,10 @@ export const createConsoleLogProgressReporter = (group?: string): ProgressReport
|
|||
},
|
||||
end () {
|
||||
console.log(group ? `[${group}] done` : 'done')
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
console.error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -191,6 +210,10 @@ export const createWrappedProgressReporter = (reporter: ProgressReporter, messag
|
|||
if (message) {
|
||||
reporter.endStage(stage)
|
||||
}
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
reporter.error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -200,6 +223,8 @@ export const createNullProgressReporter = (): ProgressReporter => {
|
|||
setMessage (message: string) {
|
||||
},
|
||||
end () {
|
||||
},
|
||||
error (message: string) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,15 +85,15 @@ const registeredJeiChannel = () => {
|
|||
[
|
||||
{
|
||||
name: 'id',
|
||||
type: 'pstring',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
{
|
||||
name: 'categoryTitle',
|
||||
type: 'pstring',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
type: 'pstring',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
|||
import type { OptionsGroupType } from './optionsGuiScheme'
|
||||
import { appQueryParams } from './appParams'
|
||||
import { options, disabledSettings } from './optionsStorage'
|
||||
import { AppConfig } from './appConfig'
|
||||
|
||||
// todo: refactor structure with support of hideNext=false
|
||||
|
||||
|
|
@ -110,26 +111,6 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY })
|
|||
|
||||
// ---
|
||||
|
||||
export type AppConfig = {
|
||||
// defaultHost?: string
|
||||
// defaultHostSave?: string
|
||||
defaultProxy?: string
|
||||
// defaultProxySave?: string
|
||||
// defaultVersion?: string
|
||||
peerJsServer?: string
|
||||
peerJsServerFallback?: string
|
||||
promoteServers?: Array<{ ip, description, version? }>
|
||||
mapsProvider?: string
|
||||
|
||||
appParams?: Record<string, any> // query string params
|
||||
|
||||
defaultSettings?: Record<string, any>
|
||||
forceSettings?: Record<string, boolean>
|
||||
// hideSettings?: Record<string, boolean>
|
||||
allowAutoConnect?: boolean
|
||||
pauseLinks?: Array<Array<Record<string, any>>>
|
||||
}
|
||||
|
||||
export const miscUiState = proxy({
|
||||
currentDisplayQr: null as string | null,
|
||||
currentTouch: null as boolean | null,
|
||||
|
|
@ -144,7 +125,7 @@ export const miscUiState = proxy({
|
|||
loadedServerIndex: '',
|
||||
/** currently trying to load or loaded mc version, after all data is loaded */
|
||||
loadedDataVersion: null as string | null,
|
||||
appLoaded: false,
|
||||
fsReady: false,
|
||||
singleplayerAvailable: false,
|
||||
usingGamepadInput: false,
|
||||
appConfig: null as AppConfig | null,
|
||||
|
|
@ -152,24 +133,6 @@ export const miscUiState = proxy({
|
|||
displayFullmap: false
|
||||
})
|
||||
|
||||
export const loadAppConfig = (appConfig: AppConfig) => {
|
||||
if (miscUiState.appConfig) {
|
||||
Object.assign(miscUiState.appConfig, appConfig)
|
||||
} else {
|
||||
miscUiState.appConfig = appConfig
|
||||
}
|
||||
|
||||
if (appConfig.forceSettings) {
|
||||
for (const [key, value] of Object.entries(appConfig.forceSettings)) {
|
||||
if (value) {
|
||||
disabledSettings.value.delete(key)
|
||||
} else {
|
||||
disabledSettings.value.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isGameActive = (foregroundCheck: boolean) => {
|
||||
if (foregroundCheck && activeModalStack.length) return false
|
||||
return miscUiState.gameLoaded
|
||||
|
|
|
|||
17
src/index.ts
17
src/index.ts
|
|
@ -12,6 +12,7 @@ import './mineflayer/cameraShake'
|
|||
import './shims/patchShims'
|
||||
import './mineflayer/java-tester/index'
|
||||
import './external'
|
||||
import './appConfig'
|
||||
import { getServerInfo } from './mineflayer/mc-protocol'
|
||||
import { onGameLoad, renderSlot } from './inventoryWindows'
|
||||
import { GeneralInputItem } from './mineflayer/items'
|
||||
|
|
@ -46,7 +47,6 @@ import initializePacketsReplay from './packetsReplay/packetsReplayLegacy'
|
|||
|
||||
import { initVR } from './vr'
|
||||
import {
|
||||
AppConfig,
|
||||
activeModalStack,
|
||||
activeModalStacks,
|
||||
hideModal,
|
||||
|
|
@ -55,7 +55,6 @@ import {
|
|||
miscUiState,
|
||||
showModal,
|
||||
gameAdditionalState,
|
||||
loadAppConfig
|
||||
} from './globalState'
|
||||
|
||||
import { parseServerAddress } from './parseServerAddress'
|
||||
|
|
@ -836,8 +835,9 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined
|
||||
|
||||
listenGlobalEvents()
|
||||
watchValue(miscUiState, async s => {
|
||||
if (s.appLoaded) { // fs ready
|
||||
const unsubscribe = watchValue(miscUiState, async s => {
|
||||
if (s.fsReady && s.appConfig) {
|
||||
unsubscribe()
|
||||
if (reconnectOptions) {
|
||||
sessionStorage.removeItem('reconnectOptions')
|
||||
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
|
||||
|
|
@ -898,15 +898,6 @@ document.body.addEventListener('touchstart', (e) => {
|
|||
}, { passive: false })
|
||||
// #endregion
|
||||
|
||||
loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {})
|
||||
// load maybe updated config on the server with updated params (just in case)
|
||||
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
|
||||
console.warn('Failed to load optional app config.json', error)
|
||||
return {}
|
||||
}).then((config: AppConfig | {}) => {
|
||||
loadAppConfig(config)
|
||||
})
|
||||
|
||||
// qs open actions
|
||||
if (!reconnectOptions) {
|
||||
downloadAndOpenFile().then((downloadAction) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { versionToNumber } from 'renderer/viewer/common/utils'
|
|||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer'
|
||||
import Generic95 from '../assets/generic_95.png'
|
||||
import { appReplacableResources } from './generated/resources'
|
||||
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
|
||||
|
|
@ -155,7 +156,10 @@ const getImageSrc = (path): string | HTMLImageElement => {
|
|||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
||||
}
|
||||
|
||||
const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => { }) => {
|
||||
const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any, image = undefined as HTMLImageElement | undefined }, onLoad = () => { }) => {
|
||||
if (image) {
|
||||
return image
|
||||
}
|
||||
if (!path && !texture) throw new Error('Either pass path or texture')
|
||||
const loadPath = (blockData ? 'blocks' : path ?? texture)!
|
||||
if (loadedImagesCache.has(loadPath)) {
|
||||
|
|
@ -184,7 +188,8 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal
|
|||
blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel },
|
||||
scale?: number,
|
||||
slice?: number[],
|
||||
modelName?: string
|
||||
modelName?: string,
|
||||
image?: HTMLImageElement
|
||||
} | undefined => {
|
||||
let itemModelName = model.modelName
|
||||
const originalItemName = itemModelName
|
||||
|
|
@ -196,6 +201,23 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal
|
|||
|
||||
|
||||
let itemTexture
|
||||
|
||||
if (!fullBlockModelSupport) {
|
||||
const atlas = activeGuiAtlas.atlas?.json
|
||||
// todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works)
|
||||
const item = atlas?.textures[itemModelName.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '').replace('_bottom', '')]
|
||||
if (item) {
|
||||
const x = item.u * atlas.width
|
||||
const y = item.v * atlas.height
|
||||
return {
|
||||
texture: 'gui',
|
||||
image: activeGuiAtlas.atlas!.image,
|
||||
slice: [x, y, atlas.tileSize, atlas.tileSize],
|
||||
scale: 0.25,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assertDefined(viewer.world.itemsRenderer)
|
||||
itemTexture =
|
||||
|
|
@ -205,6 +227,8 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal
|
|||
inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
||||
itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')!
|
||||
}
|
||||
|
||||
|
||||
if ('type' in itemTexture) {
|
||||
// is item
|
||||
return {
|
||||
|
|
@ -230,13 +254,13 @@ const getItemName = (slot: Item | RenderItem | null) => {
|
|||
return text.join('')
|
||||
}
|
||||
|
||||
const mapSlots = (slots: Array<RenderItem | Item | null>) => {
|
||||
const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
|
||||
return slots.map((slot, i) => {
|
||||
// todo stateid
|
||||
if (!slot) return
|
||||
|
||||
try {
|
||||
const debugIsQuickbar = i === bot.inventory.hotbarStart + bot.quickBarSlot
|
||||
const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot
|
||||
const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', })
|
||||
const slotCustomProps = renderSlot({ modelName }, debugIsQuickbar)
|
||||
const itemCustomName = getItemName(slot)
|
||||
|
|
@ -305,7 +329,7 @@ const upJei = (search: string) => {
|
|||
return new PrismarineItem(x.id, 1)
|
||||
}).filter(a => a !== null)
|
||||
lastWindow.pwindow.win.jeiSlotsPage = 0
|
||||
lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots)
|
||||
lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots, true)
|
||||
}
|
||||
|
||||
export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const pingServerVersion = async (ip: string, port?: number, mergeOptions:
|
|||
...mergeOptions,
|
||||
}
|
||||
let latency = 0
|
||||
let fullInfo = null
|
||||
let fullInfo: any = null
|
||||
fakeClient.autoVersionHooks = [(res) => {
|
||||
latency = res.latency
|
||||
fullInfo = res
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { proxy, subscribe } from 'valtio/vanilla'
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { omitObj } from '@zardoy/utils'
|
||||
import { appQueryParamsArray } from './appParams'
|
||||
import type { AppConfig } from './globalState'
|
||||
import type { AppConfig } from './appConfig'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
|
||||
|
|
@ -188,7 +188,7 @@ subscribe(options, () => {
|
|||
localStorage.options = JSON.stringify(saveOptions)
|
||||
})
|
||||
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => void
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void
|
||||
|
||||
export const watchValue: WatchValue = (proxy, callback) => {
|
||||
const watchedProps = new Set<string>()
|
||||
|
|
@ -198,10 +198,19 @@ export const watchValue: WatchValue = (proxy, callback) => {
|
|||
return Reflect.get(target, p, receiver)
|
||||
},
|
||||
}), false)
|
||||
const unsubscribes = [] as Array<() => void>
|
||||
for (const prop of watchedProps) {
|
||||
subscribeKey(proxy, prop, () => {
|
||||
callback(proxy, true)
|
||||
})
|
||||
unsubscribes.push(
|
||||
subscribeKey(proxy, prop, () => {
|
||||
callback(proxy, true)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const unsubscribe of unsubscribes) {
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { activeModalStack, miscUiState } from '../globalState'
|
|||
import Button from './Button'
|
||||
import { useUsingTouch } from './utilsApp'
|
||||
import { pixelartIcons } from './PixelartIcon'
|
||||
import { showNotification } from './NotificationProvider'
|
||||
|
||||
const hideOnModals = new Set(['chat'])
|
||||
|
||||
|
|
@ -33,8 +34,12 @@ export default () => {
|
|||
left: inMainMenu ? 35 : 5,
|
||||
width: 22,
|
||||
}}
|
||||
onClick={() => {
|
||||
void document.documentElement.requestFullscreen()
|
||||
onClick={async () => {
|
||||
try {
|
||||
await document.documentElement.requestFullscreen()
|
||||
} catch (err) {
|
||||
showNotification(`${err.message ?? err}`, undefined, true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,12 +75,12 @@ export const mainMenuState = proxy({
|
|||
let disableAnimation = false
|
||||
export default () => {
|
||||
const haveModals = useSnapshot(activeModalStack).length
|
||||
const { gameLoaded, appLoaded, appConfig, singleplayerAvailable } = useSnapshot(miscUiState)
|
||||
const { gameLoaded, fsReady, appConfig, singleplayerAvailable } = useSnapshot(miscUiState)
|
||||
|
||||
const noDisplay = haveModals || gameLoaded || !appLoaded
|
||||
const noDisplay = haveModals || gameLoaded || !fsReady
|
||||
|
||||
useEffect(() => {
|
||||
if (noDisplay && appLoaded) disableAnimation = true
|
||||
if (noDisplay && fsReady) disableAnimation = true
|
||||
}, [noDisplay])
|
||||
|
||||
const [versionStatus, setVersionStatus] = useState('')
|
||||
|
|
|
|||
|
|
@ -60,10 +60,13 @@ export default () => {
|
|||
// }, [])
|
||||
const scale = useAppScale()
|
||||
|
||||
return <div style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top right',
|
||||
}}>
|
||||
return <div
|
||||
className='notification-container'
|
||||
style={{
|
||||
// transform: `scale(${scale})`,
|
||||
// transformOrigin: 'top right',
|
||||
}}
|
||||
>
|
||||
<Notification
|
||||
action={action}
|
||||
type={type}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useCopyKeybinding } from './simpleHooks'
|
|||
import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList, StoreServerItem } from './serversStorage'
|
||||
|
||||
type AdditionalDisplayData = {
|
||||
textNameRightGrayed: string
|
||||
formattedText: string
|
||||
textNameRight: string
|
||||
icon?: string
|
||||
|
|
@ -143,9 +144,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
let data
|
||||
if (isWebSocket) {
|
||||
const pingResult = await getServerInfo(server.ip, undefined, undefined, true)
|
||||
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
|
||||
data = {
|
||||
formattedText: `${pingResult.version} server with a direct websocket connection`,
|
||||
formattedText: pingResult.fullInfo.description,
|
||||
textNameRight: `ws ${pingResult.latency}ms`,
|
||||
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
|
||||
offline: false
|
||||
}
|
||||
} else {
|
||||
|
|
@ -364,6 +367,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
detail: (server.versionOverride ?? '') + ' ' + (server.usernameOverride ?? ''),
|
||||
formattedTextOverride: additional?.formattedText,
|
||||
worldNameRight: additional?.textNameRight ?? '',
|
||||
worldNameRightGrayed: additional?.textNameRightGrayed ?? '',
|
||||
iconSrc: additional?.icon,
|
||||
offline: additional?.offline
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,14 @@ export interface WorldProps {
|
|||
detail?: string
|
||||
formattedTextOverride?: string
|
||||
worldNameRight?: string
|
||||
worldNameRightGrayed?: string
|
||||
onFocus?: (name: string) => void
|
||||
onInteraction?(interaction: 'enter' | 'space')
|
||||
elemRef?: React.Ref<HTMLDivElement>
|
||||
offline?: boolean
|
||||
}
|
||||
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef, offline }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, worldNameRightGrayed, elemRef, offline }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
const timeRelativeFormatted = useMemo(() => {
|
||||
if (!lastPlayed) return ''
|
||||
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||
|
|
@ -63,6 +64,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
<div className={styles.world_title}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.world_title_right}>
|
||||
{worldNameRightGrayed && <span style={{ color: '#878787', fontSize: 8 }}>{worldNameRightGrayed}</span>}
|
||||
{offline ? (
|
||||
<span style={{ color: 'red', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<PixelartIcon iconName="signal-off" width={12} />
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@
|
|||
.world_title_right {
|
||||
color: #999;
|
||||
font-size: 9px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 1px;
|
||||
}
|
||||
.world_info {
|
||||
margin-left: 3px;
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ const App = () => {
|
|||
<SignInMessageProvider />
|
||||
<NoModalFoundProvider />
|
||||
<PacketsReplayProvider />
|
||||
<NotificationProvider />
|
||||
</RobustPortal>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-top-scaled'>
|
||||
|
|
@ -227,7 +228,6 @@ const App = () => {
|
|||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
<NotificationProvider />
|
||||
</RobustPortal>
|
||||
</ButtonAppProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres
|
|||
const response = await fetch(url).catch((err) => {
|
||||
console.log(`Ensure server on ${url} support CORS which is not required for regular client, but is required for the web client`)
|
||||
console.error(err)
|
||||
showNotification('Failed to download resource pack: ' + err.message)
|
||||
progressReporter.error('Failed to download resource pack: ' + err.message)
|
||||
})
|
||||
console.timeEnd('downloadServerResourcePack')
|
||||
if (!response) return
|
||||
|
|
@ -426,6 +426,8 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres
|
|||
console.error(err)
|
||||
showNotification('Failed to install resource pack: ' + err.message)
|
||||
})
|
||||
} catch (err) {
|
||||
progressReporter.error('Could not install resource pack: ' + err.message)
|
||||
} finally {
|
||||
progressReporter.endStage('download-resource-pack')
|
||||
resourcePackState.isServerInstalling = false
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpec
|
|||
const itemSelector = playerState.getItemSelector({
|
||||
...specificProps
|
||||
})
|
||||
const model = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
const modelFromDef = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
name: itemModelName,
|
||||
version: viewer.world.texturesVersion!,
|
||||
properties: itemSelector
|
||||
})?.model ?? itemModelName
|
||||
})?.model
|
||||
const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName
|
||||
return model
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
})
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue