Compare commits
10 commits
cursor/upd
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253e094c74 | ||
|
|
fef94f03fb | ||
|
|
e9f91f8ecd | ||
|
|
634df8d03d |
||
|
|
a88c8b5470 | ||
|
|
f51254d97a | ||
|
|
05cd560d6b | ||
|
|
b239636356 | ||
|
|
4f421ae45f | ||
|
|
3b94889bed |
17 changed files with 173 additions and 33 deletions
|
|
@ -78,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
|
|||
|
||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||
|
||||
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||
|
||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||
|
||||
```mermaid
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type AppConfig = {
|
|||
// defaultVersion?: string
|
||||
peerJsServer?: string
|
||||
peerJsServerFallback?: string
|
||||
promoteServers?: Array<{ ip, description, version? }>
|
||||
promoteServers?: Array<{ ip, description, name?, version?, }>
|
||||
mapsProvider?: string
|
||||
|
||||
appParams?: Record<string, any> // query string params
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ let audioContext: AudioContext
|
|||
const sounds: Record<string, any> = {}
|
||||
|
||||
// Track currently playing sounds and their gain nodes
|
||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
||||
const activeSounds: Array<{
|
||||
source: AudioBufferSourceNode;
|
||||
gainNode: GainNode;
|
||||
volumeMultiplier: number;
|
||||
isMusic: boolean;
|
||||
}> = []
|
||||
window.activeSounds = activeSounds
|
||||
|
||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||
|
|
@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) {
|
|||
}
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => {
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => {
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
const start = Date.now()
|
||||
|
|
@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option
|
|||
if (cancelled || Date.now() - start > loadTimeout) return
|
||||
}
|
||||
|
||||
return playSound(url, soundVolume, loop)
|
||||
return playSound(url, soundVolume, loop, isMusic)
|
||||
}
|
||||
|
||||
export async function playSound (url, soundVolume = 1, loop = false) {
|
||||
const volume = soundVolume * (options.volume / 100)
|
||||
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) {
|
||||
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1)
|
||||
|
||||
if (!volume) return
|
||||
|
||||
|
|
@ -82,7 +87,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
|||
source.start(0)
|
||||
|
||||
// Add to active sounds
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic })
|
||||
|
||||
const callbacks = [] as Array<() => void>
|
||||
source.onended = () => {
|
||||
|
|
@ -110,6 +115,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
|||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
},
|
||||
gainNode,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,11 +143,11 @@ export function stopSound (url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
|
||||
const normalizedVolume = newVolume / 100
|
||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
||||
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) {
|
||||
try {
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1)
|
||||
} catch (err) {
|
||||
console.warn('Failed to change sound volume:', err)
|
||||
}
|
||||
|
|
@ -149,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
|||
}
|
||||
|
||||
subscribeKey(options, 'volume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||
})
|
||||
|
||||
subscribeKey(options, 'musicVolume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export const defaultOptions = {
|
|||
chatOpacityOpened: 100,
|
||||
messagesLimit: 200,
|
||||
volume: 50,
|
||||
enableMusic: false,
|
||||
enableMusic: true,
|
||||
musicVolume: 50,
|
||||
// fov: 70,
|
||||
fov: 75,
|
||||
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
|
|||
import { enable, disable, enabled } from 'debug'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => {
|
||||
name = name.replace('packet_', '')
|
||||
return [name, name]
|
||||
}))
|
||||
window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => {
|
||||
name = name.replace('packet_', '')
|
||||
return [name, name]
|
||||
}))
|
||||
})
|
||||
|
||||
window.Vec3 = Vec3
|
||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
||||
|
|
|
|||
|
|
@ -246,22 +246,29 @@ customEvents.on('gameLoaded', () => {
|
|||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
reportError(new Error('Error applying skin texture:', { cause: err }))
|
||||
}
|
||||
}
|
||||
|
||||
bot.on('playerJoined', updateSkin)
|
||||
bot.on('playerUpdated', updateSkin)
|
||||
for (const entity of Object.values(bot.players)) {
|
||||
updateSkin(entity)
|
||||
}
|
||||
|
||||
bot.on('teamUpdated', (team: Team) => {
|
||||
const teamUpdated = (team: Team) => {
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
|
||||
bot.emit('entityUpdate', entity)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
bot.on('teamUpdated', teamUpdated)
|
||||
for (const team of Object.values(bot.teams)) {
|
||||
teamUpdated(team)
|
||||
}
|
||||
|
||||
const updateEntityNameTags = (team: Team) => {
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@ class CustomDuplex extends Duplex {
|
|||
}
|
||||
|
||||
export const getWebsocketStream = async (host: string) => {
|
||||
const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss'
|
||||
const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss'
|
||||
const hostClean = host.replace('ws://', '').replace('wss://', '')
|
||||
const ws = new WebSocket(`${baseProtocol}://${hostClean}`)
|
||||
const hostURL = new URL(`${baseProtocol}://${hostClean}`)
|
||||
const hostParams = hostURL.searchParams
|
||||
hostParams.append('client_mcraft', '')
|
||||
const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`)
|
||||
const clientDuplex = new CustomDuplex(undefined, data => {
|
||||
ws.send(data)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -480,6 +480,24 @@ export const guiOptionsScheme: {
|
|||
],
|
||||
sound: [
|
||||
{ volume: {} },
|
||||
{
|
||||
custom () {
|
||||
return <OptionSlider
|
||||
valueOverride={options.enableMusic ? undefined : 0}
|
||||
onChange={(value) => {
|
||||
options.musicVolume = value
|
||||
}}
|
||||
item={{
|
||||
type: 'slider',
|
||||
id: 'musicVolume',
|
||||
text: 'Music Volume',
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
}}
|
||||
/>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export const startLocalReplayServer = (contents: string) => {
|
|||
const server = createServer({
|
||||
Server: LocalServer as any,
|
||||
version: header.minecraftVersion,
|
||||
keepAlive: false,
|
||||
'online-mode': false
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}
|
||||
|
||||
const displayConnectButton = qsParamIp
|
||||
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg']
|
||||
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun']
|
||||
// pick random example
|
||||
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ export default ({
|
|||
const chatInput = useRef<HTMLInputElement>(null!)
|
||||
const chatMessages = useRef<HTMLDivElement>(null)
|
||||
const chatHistoryPos = useRef(sendHistoryRef.current.length)
|
||||
const commandHistoryPos = useRef(0)
|
||||
const inputCurrentlyEnteredValue = useRef('')
|
||||
const commandHistoryRef = useRef(sendHistoryRef.current.filter((msg: string) => msg.startsWith('/')))
|
||||
|
||||
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
|
||||
const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
|
||||
|
|
@ -142,6 +144,9 @@ export default ({
|
|||
sendHistoryRef.current = newHistory
|
||||
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
|
||||
chatHistoryPos.current = newHistory.length
|
||||
// Update command history (only messages starting with /)
|
||||
commandHistoryRef.current = newHistory.filter((msg: string) => msg.startsWith('/'))
|
||||
commandHistoryPos.current = commandHistoryRef.current.length
|
||||
}
|
||||
|
||||
const acceptComplete = (item: string) => {
|
||||
|
|
@ -180,6 +185,21 @@ export default ({
|
|||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
}
|
||||
|
||||
const handleCommandArrowUp = () => {
|
||||
if (commandHistoryPos.current === 0 || commandHistoryRef.current.length === 0) return
|
||||
if (commandHistoryPos.current === commandHistoryRef.current.length) { // started navigating command history
|
||||
inputCurrentlyEnteredValue.current = chatInput.current.value
|
||||
}
|
||||
commandHistoryPos.current--
|
||||
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || '')
|
||||
}
|
||||
|
||||
const handleCommandArrowDown = () => {
|
||||
if (commandHistoryPos.current === commandHistoryRef.current.length) return
|
||||
commandHistoryPos.current++
|
||||
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
}
|
||||
|
||||
const auxInputFocus = (direction: 'up' | 'down') => {
|
||||
chatInput.current.focus()
|
||||
if (direction === 'up') {
|
||||
|
|
@ -203,6 +223,7 @@ export default ({
|
|||
updateInputValue(chatInputValueGlobal.value)
|
||||
chatInputValueGlobal.value = ''
|
||||
chatHistoryPos.current = sendHistoryRef.current.length
|
||||
commandHistoryPos.current = commandHistoryRef.current.length
|
||||
if (!usingTouch) {
|
||||
chatInput.current.focus()
|
||||
}
|
||||
|
|
@ -524,9 +545,19 @@ export default ({
|
|||
onBlur={() => setIsInputFocused(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'ArrowUp') {
|
||||
handleArrowUp()
|
||||
if (e.altKey) {
|
||||
handleCommandArrowUp()
|
||||
e.preventDefault()
|
||||
} else {
|
||||
handleArrowUp()
|
||||
}
|
||||
} else if (e.code === 'ArrowDown') {
|
||||
handleArrowDown()
|
||||
if (e.altKey) {
|
||||
handleCommandArrowDown()
|
||||
e.preventDefault()
|
||||
} else {
|
||||
handleArrowDown()
|
||||
}
|
||||
}
|
||||
if (e.code === 'Tab') {
|
||||
if (completionItemsSource.length) {
|
||||
|
|
|
|||
|
|
@ -161,7 +161,15 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
|
|||
/>
|
||||
}
|
||||
|
||||
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
|
||||
export const OptionSlider = ({
|
||||
item,
|
||||
onChange,
|
||||
valueOverride
|
||||
}: {
|
||||
item: Extract<OptionMeta, { type: 'slider' }>
|
||||
onChange?: (value: number) => void
|
||||
valueOverride?: number
|
||||
}) => {
|
||||
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
||||
|
||||
const optionValue = useSnapshot(options)[item.id!]
|
||||
|
|
@ -174,7 +182,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
|||
return (
|
||||
<Slider
|
||||
label={item.text!}
|
||||
value={options[item.id!]}
|
||||
value={valueOverride ?? options[item.id!]}
|
||||
data-setting={item.id}
|
||||
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
|
||||
min={item.min}
|
||||
|
|
@ -184,6 +192,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
|||
updateOnDragEnd={item.delayApply}
|
||||
updateValue={(value) => {
|
||||
options[item.id!] = value
|
||||
onChange?.(value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -119,11 +119,15 @@ export default () => {
|
|||
modelLoaders.current.set(modelUrl, loader)
|
||||
|
||||
const onLoad = (object: THREE.Object3D) => {
|
||||
// Apply customization if available
|
||||
// Apply customization if available and enable shadows
|
||||
const customization = model?.modelCustomization?.[modelUrl]
|
||||
if (customization) {
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh && child.material) {
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
// Enable shadow casting and receiving for all meshes
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
|
||||
if (child.material && customization) {
|
||||
const material = child.material as THREE.MeshStandardMaterial
|
||||
if (customization.color) {
|
||||
material.color.setHex(parseInt(customization.color.replace('#', ''), 16))
|
||||
|
|
@ -139,8 +143,8 @@ export default () => {
|
|||
material.roughness = customization.roughness
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Center and scale model
|
||||
const box = new THREE.Box3().setFromObject(object)
|
||||
|
|
@ -259,6 +263,12 @@ export default () => {
|
|||
}
|
||||
renderer.setPixelRatio(scale)
|
||||
renderer.setSize(model.positioning.width, model.positioning.height)
|
||||
|
||||
// Enable shadow rendering for depth and realism
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap // Soft shadows for better quality
|
||||
renderer.shadowMap.autoUpdate = true
|
||||
|
||||
containerRef.current.appendChild(renderer.domElement)
|
||||
|
||||
// Setup controls
|
||||
|
|
@ -270,10 +280,30 @@ export default () => {
|
|||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
|
||||
// Add ambient light
|
||||
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 1)
|
||||
// Add ambient light for overall illumination
|
||||
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 0.4) // Reduced intensity to allow shadows
|
||||
scene.add(ambientLight)
|
||||
|
||||
// Add directional light for shadows and depth (similar to Minecraft inventory lighting)
|
||||
const directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.6)
|
||||
directionalLight.position.set(2, 2, 2) // Position light from top-right-front
|
||||
directionalLight.target.position.set(0, 0, 0) // Point towards center of scene
|
||||
|
||||
// Configure shadow properties for optimal quality
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.width = 2048 // High resolution shadow map
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
directionalLight.shadow.camera.near = 0.1
|
||||
directionalLight.shadow.camera.far = 10
|
||||
directionalLight.shadow.camera.left = -3
|
||||
directionalLight.shadow.camera.right = 3
|
||||
directionalLight.shadow.camera.top = 3
|
||||
directionalLight.shadow.camera.bottom = -3
|
||||
directionalLight.shadow.bias = -0.0001 // Reduce shadow acne
|
||||
|
||||
scene.add(directionalLight)
|
||||
scene.add(directionalLight.target)
|
||||
|
||||
// Cursor following function
|
||||
const updatePlayerLookAt = () => {
|
||||
if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return
|
||||
|
|
@ -342,6 +372,14 @@ export default () => {
|
|||
scale: 1 // Start with base scale, will adjust below
|
||||
})
|
||||
|
||||
// Enable shadows for player object
|
||||
wrapper.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate proper scale and positioning for camera view
|
||||
const box = new THREE.Box3().setFromObject(wrapper)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
...serversListProvided,
|
||||
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
|
||||
ip: server.ip,
|
||||
name: server.name,
|
||||
versionOverride: server.version,
|
||||
description: server.description,
|
||||
isRecommended: true
|
||||
|
|
@ -167,6 +168,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
|
||||
data = {
|
||||
formattedText: pingResult.fullInfo.description,
|
||||
icon: pingResult.fullInfo.favicon,
|
||||
textNameRight: `ws ${pingResult.latency}ms`,
|
||||
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
|
||||
offline: false
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ class MusicSystem {
|
|||
private currentMusic: string | null = null
|
||||
|
||||
async playMusic (url: string, musicVolume = 1) {
|
||||
if (!options.enableMusic || this.currentMusic) return
|
||||
if (!options.enableMusic || this.currentMusic || options.musicVolume === 0) return
|
||||
|
||||
try {
|
||||
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
|
||||
const { onEnded } = await loadOrPlaySound(url, musicVolume, 5000, undefined, true) ?? {}
|
||||
|
||||
if (!onEnded) return
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
||||
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { reloadChunks } from './utils'
|
||||
import { miscUiState } from './globalState'
|
||||
|
|
@ -97,6 +98,8 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
|
||||
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
|
||||
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
|
||||
|
||||
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
|
||||
})
|
||||
|
||||
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue