Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions renderer/viewer/lib/skyLight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Calculates sky light level based on Minecraft time of day.
*
* Minecraft time reference:
* - 0 ticks = 6:00 AM (sunrise complete)
* - 6000 ticks = 12:00 PM (noon) - brightest
* - 12000 ticks = 6:00 PM (sunset begins)
* - 13000 ticks = 7:00 PM (dusk/night begins)
* - 18000 ticks = 12:00 AM (midnight) - darkest
* - 23000 ticks = 5:00 AM (dawn begins)
* - 24000 ticks = 6:00 AM (same as 0)
*
* Sky light ranges from 4 (night) to 15 (day).
*/

/**
* Calculate celestial angle from time of day (0-1 range representing sun position)
*/
export const getCelestialAngle = (timeOfDay: number): number => {
// Normalize time to 0-1 range
let angle = ((timeOfDay % 24_000) / 24_000) - 0.25

if (angle < 0) angle += 1
if (angle > 1) angle -= 1

// Vanilla Minecraft applies a smoothing curve
const smoothedAngle = angle + (1 - Math.cos(angle * Math.PI)) / 2
return smoothedAngle
}

/**
* Calculate sky light level (0-15) based on time of day in ticks.
* Matches Minecraft vanilla behavior.
*
* @param timeOfDay - Time in ticks (0-24000)
* @returns Sky light level (4-15, where 15 is brightest day, 4 is darkest night)
*/
export const calculateSkyLight = (timeOfDay: number): number => {
// Normalize time to 0-24000 range
const normalizedTime = ((timeOfDay % 24_000) + 24_000) % 24_000

// Calculate celestial angle (0-1, where 0.25 is noon, 0.75 is midnight)
const celestialAngle = getCelestialAngle(normalizedTime)

// Calculate brightness factor based on celestial angle
// cos gives us smooth day/night transition
const cos = Math.cos(celestialAngle * Math.PI * 2)

// Map cos (-1 to 1) to brightness (0 to 1)
// At noon (celestialAngle ~0.25): cos(0.5π) = 0, but we want max brightness
// At midnight (celestialAngle ~0.75): cos(1.5π) = 0, but we want min brightness

// Vanilla-like calculation:
// brightness goes from 0 (dark) to 1 (bright)
const brightness = cos * 0.5 + 0.5

// Apply threshold - night should be darker
// Vanilla has minimum sky light of 4 during night
const skyLight = Math.round(4 + brightness * 11)

return Math.max(4, Math.min(15, skyLight))
}

/**
* Simplified sky light calculation that more closely matches vanilla behavior.
* Uses piecewise linear interpolation based on known Minecraft light levels.
*
* @param timeOfDay - Time in ticks (0-24000)
* @returns Sky light level (4-15)
*/
export const calculateSkyLightSimple = (timeOfDay: number): number => {
// Normalize to 0-24000
const time = ((timeOfDay % 24_000) + 24_000) % 24_000

// Vanilla Minecraft approximate sky light levels:
// 0-12000 (6AM-6PM): Day, sky light = 15
// 12000-13000 (6PM-7PM): Sunset transition, 15 -> 4
// 13000-23000 (7PM-5AM): Night, sky light = 4
// 23000-24000 (5AM-6AM): Sunrise transition, 4 -> 15

if (time >= 0 && time < 12_000) {
// Day time - full brightness
return 15
} else if (time >= 12_000 && time < 13_000) {
// Sunset transition (6PM to 7PM)
const progress = (time - 12_000) / 1000
return Math.round(15 - progress * 11)
} else if (time >= 13_000 && time < 23_000) {
// Night time - minimum brightness
return 4
} else {
// Sunrise transition (5AM to 6AM)
const progress = (time - 23_000) / 1000
return Math.round(4 + progress * 11)
}
}

// Test/debug helper - run this to see values at different times
export const debugSkyLight = () => {
const testTimes = [
{ ticks: 0, label: '6:00 AM (sunrise)' },
{ ticks: 6000, label: '12:00 PM (noon)' },
{ ticks: 12_000, label: '6:00 PM (sunset starts)' },
{ ticks: 12_500, label: '6:30 PM (sunset mid)' },
{ ticks: 13_000, label: '7:00 PM (night begins)' },
{ ticks: 18_000, label: '12:00 AM (midnight)' },
{ ticks: 19_000, label: '1:00 AM' },
{ ticks: 23_000, label: '5:00 AM (dawn begins)' },
{ ticks: 23_500, label: '5:30 AM (dawn mid)' },
]

console.log('Sky Light Debug:')
console.log('================')
for (const { ticks, label } of testTimes) {
const smooth = calculateSkyLight(ticks)
const simple = calculateSkyLightSimple(ticks)
console.log(`${ticks.toString().padStart(5)} ticks (${label}): smooth=${smooth}, simple=${simple}`)
}
}

// Export for global access in console
if (typeof window !== 'undefined') {
(window as any).debugSkyLight = debugSkyLight
}
14 changes: 2 additions & 12 deletions renderer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { WorldDataEmitterWorker } from './worldDataEmitter'
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
import { MesherLogReader } from './mesherlogReader'
import { setSkinsConfig } from './utils/skins'
import { calculateSkyLightSimple } from './skyLight'

function mod (x, n) {
return ((x % n) + n) % n
Expand Down Expand Up @@ -564,19 +565,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}

getMesherConfig (): MesherConfig {
let skyLight = 15
const timeOfDay = this.timeOfTheDay
if (timeOfDay < 0 || timeOfDay > 24_000) {
//
} else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
skyLight = 15
} else if (timeOfDay > 6000 && timeOfDay < 12_000) {
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
} else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
skyLight = ((timeOfDay - 12_000) / 6000) * 15
}

skyLight = Math.floor(skyLight)
const skyLight = (timeOfDay < 0 || timeOfDay > 24_000) ? 15 : calculateSkyLightSimple(timeOfDay)
return {
version: this.version,
enableLighting: this.worldRendererConfig.enableLighting,
Expand Down
8 changes: 6 additions & 2 deletions renderer/viewer/three/waypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
y: number
z: number
minDistance: number
maxDistance: number
color: number
label?: string
sprite: WaypointSprite
Expand All @@ -17,6 +18,7 @@
color?: number
label?: string
minDistance?: number
maxDistance?: number
metadata?: any
}

Expand Down Expand Up @@ -54,7 +56,8 @@
for (const waypoint of this.waypoints.values()) {
const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
const distance = playerPos.distanceTo(waypointPos)
const visible = !waypoint.minDistance || distance >= waypoint.minDistance
const visible = (!waypoint.minDistance || distance >= waypoint.minDistance) &&
(waypoint.maxDistance === Infinity || distance <= waypoint.maxDistance)

Check failure on line 60 in renderer/viewer/three/waypoints.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Expected indentation of 8 spaces

waypoint.sprite.setVisible(visible)

Expand Down Expand Up @@ -92,6 +95,7 @@
const color = options.color ?? 0xFF_00_00
const { label, metadata } = options
const minDistance = options.minDistance ?? 0
const maxDistance = options.maxDistance ?? Infinity

const sprite = createWaypointSprite({
position: new THREE.Vector3(x, y, z),
Expand All @@ -105,7 +109,7 @@
this.waypointScene.add(sprite.group)

this.waypoints.set(id, {
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance, maxDistance,
color, label,
sprite,
})
Expand Down
7 changes: 7 additions & 0 deletions src/appStatus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { resetStateAfterDisconnect } from './browserfs'
import type { ConnectOptions } from './connect'
import { hideModal, activeModalStack, showModal, miscUiState } from './globalState'
import { appStatusState, resetAppStatusState } from './react/AppStatusProvider'

Expand Down Expand Up @@ -39,3 +40,9 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
}
}
globalThis.setLoadingScreenStatus = setLoadingScreenStatus

export const lastConnectOptions = {
value: null as ConnectOptions | null,
hadWorldLoaded: false
}
globalThis.lastConnectOptions = lastConnectOptions
2 changes: 1 addition & 1 deletion src/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { getItemFromBlock } from './chatUtils'
import { gamepadUiCursorState } from './react/GamepadUiCursor'
import { completeResourcepackPackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack'
import { showNotification } from './react/NotificationProvider'
import { lastConnectOptions } from './react/AppStatusProvider'
import { lastConnectOptions } from './appStatus'
import { onCameraMove, onControInit } from './cameraRotationControls'
import { createNotificationProgressReporter } from './core/progressReporter'
import { appStorage } from './react/appStorageProvider'
Expand Down
29 changes: 16 additions & 13 deletions src/customChannels.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import PItem from 'prismarine-item'
import * as THREE from 'three'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { options } from './optionsStorage'
import { options, serverChangedSettings } from './optionsStorage'
import { jeiCustomCategories } from './inventoryWindows'
import { registerIdeChannels } from './core/ideChannels'
import { serverSafeSettings } from './defaultOptions'
import { lastConnectOptions } from './appStatus'

const isWebSocketServer = (server: string | undefined) => {
if (!server) return false
return server.startsWith('ws://') || server.startsWith('wss://')
}

const getIsCustomChannelsEnabled = () => {
if (options.customChannels === 'websocket') return isWebSocketServer(lastConnectOptions.value?.server)
return options.customChannels
}

export default () => {
customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return
if (!getIsCustomChannelsEnabled()) return
bot.once('login', () => {
registerBlockModelsChannel()
registerMediaChannels()
Expand Down Expand Up @@ -148,6 +159,7 @@ const registerWaypointChannels = () => {

getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
minDistance: data.minDistance,
maxDistance: metadata.maxDistance,
label: data.label || undefined,
color: data.color || undefined,
metadata
Expand Down Expand Up @@ -566,17 +578,8 @@ const registerServerSettingsChannel = () => {
continue
}

// Validate type matches
const currentValue = options[key]

// For union types, check if value is valid
if (Array.isArray(currentValue) && !Array.isArray(value)) {
console.warn(`Type mismatch for setting ${key}: expected array`)
skippedCount++
continue
}

// Apply the setting
// todo remove it later, let user take control back and make clear to user
serverChangedSettings.value.add(key)
options[key] = value
appliedCount++
}
Expand Down
3 changes: 2 additions & 1 deletion src/defaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const defaultOptions = {
displayRecordButton: true,
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
customChannels: false,
customChannels: 'websocket' as boolean | 'websocket',
remoteContentNotSameOrigin: false as boolean | string[],
packetsRecordingAutoStart: false,
language: 'auto',
Expand Down Expand Up @@ -165,6 +165,7 @@ function getTouchControlsSize () {
* Settings like modsSupport, customChannels, or security-related options are excluded.
*/
export const serverSafeSettings: Partial<Record<keyof typeof defaultOptions, true>> = {
remoteContentNotSameOrigin: true, // allow server to change remote content not same origin policy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Server can override client cross-origin security policy

Adding remoteContentNotSameOrigin to serverSafeSettings allows remote servers to modify the client's cross-origin resource policy via the settings channel. This contradicts the documented intent where "security-related options are excluded" from server-controlled settings. A malicious server could set this to true to enable loading media from any origin, potentially enabling phishing attacks through arbitrary external content displayed in-game.

Fix in Cursor Fix in Web

renderEars: true,
viewBobbing: true,
mouseRawInput: true,
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
} from './globalState'

import { parseServerAddress } from './parseServerAddress'
import { setLoadingScreenStatus } from './appStatus'
import { setLoadingScreenStatus, lastConnectOptions } from './appStatus'
import { isCypress } from './standaloneUtils'

import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
Expand All @@ -64,7 +64,7 @@ import { onAppLoad, resourcepackReload, resourcePackState } from './resourcePack
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient'
import { registerServiceWorker } from './serviceWorker'
import { appStatusState, lastConnectOptions, quickDevReconnect } from './react/AppStatusProvider'
import { appStatusState, quickDevReconnect } from './react/AppStatusProvider'

import { fsState } from './loadSave'
import { watchFov } from './rendererUtils'
Expand Down
2 changes: 1 addition & 1 deletion src/mineflayer/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lastConnectOptions } from '../../react/AppStatusProvider'
import { lastConnectOptions } from '../../appStatus'
import mouse from './mouse'
import packetsPatcher from './packetsPatcher'
import { localRelayServerPlugin } from './packetsRecording'
Expand Down
2 changes: 1 addition & 1 deletion src/mineflayer/plugins/packetsRecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Bot } from 'mineflayer'
import CircularBuffer from 'flying-squid/dist/circularBuffer'
import { PacketsLogger } from 'mcraft-fun-mineflayer/build/packetsLogger'
import { subscribe } from 'valtio'
import { lastConnectOptions } from '../../react/AppStatusProvider'
import { lastConnectOptions } from '../../appStatus'
import { packetsRecordingState } from '../../packetsReplay/packetsReplayLegacy'
import { packetsReplayState } from '../../react/state/packetsReplayState'

Expand Down
5 changes: 4 additions & 1 deletion src/optionsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const qsOptions = Object.fromEntries(qsOptionsRaw.map(o => {
export const disabledSettings = proxy({
value: new Set<string>(Object.keys(qsOptions))
})
export const serverChangedSettings = proxy({
value: new Set<string>()
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Server-changed settings never cleared preventing user persistence

The serverChangedSettings set is populated when a server modifies client settings, but it's never cleared on disconnect. This prevents user-initiated changes to those settings from being persisted to local storage, even after leaving the server. The user's manual changes work in the current session but are lost on page reload, as resetStateAfterDisconnect doesn't reset this tracking state.

Additional Locations (1)

Fix in Cursor Fix in Web


const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
if (options.highPerformanceGpu) {
Expand Down Expand Up @@ -104,7 +107,7 @@ subscribe(options, (ops) => {
// for (const part of path) {
// }
const key = path[0] as string
if (disabledSettings.value.has(key)) continue
if (disabledSettings.value.has(key) || serverChangedSettings.value.has(key)) continue
appStorage.changedSettings[key] = options[key]
}
})
Expand Down
2 changes: 1 addition & 1 deletion src/react/AppStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'
import { appQueryParams } from '../appParams'
import { lastConnectOptions } from '../appStatus'
import styles from './appStatus.module.css'
import Button from './Button'
import Screen from './Screen'
import LoadingChunks from './LoadingChunks'
import LoadingTimer from './LoadingTimer'
import { lastConnectOptions } from './AppStatusProvider'
import { withInjectableUi } from './extendableSystem'

const AppStatusBase = ({
Expand Down
7 changes: 1 addition & 6 deletions src/react/AppStatusProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { downloadPacketsReplay, packetsRecordingState, replayLogger } from '../p
import { getProxyDetails } from '../microsoftAuthflow'
import { downloadAutoCapturedPackets, getLastAutoCapturedPackets } from '../mineflayer/plugins/packetsRecording'
import { appQueryParams } from '../appParams'
import { lastConnectOptions } from '../appStatus'
import AppStatus from './AppStatus'
import DiveTransition from './DiveTransition'
import { useDidUpdateEffect } from './utils'
Expand Down Expand Up @@ -36,12 +37,6 @@ export const resetAppStatusState = () => {
Object.assign(appStatusState, initialState)
}

export const lastConnectOptions = {
value: null as ConnectOptions | null,
hadWorldLoaded: false
}
globalThis.lastConnectOptions = lastConnectOptions

const saveReconnectOptions = (options: ConnectOptions) => {
sessionStorage.setItem('reconnectOptions', JSON.stringify({
value: options,
Expand Down
2 changes: 1 addition & 1 deletion src/react/ChatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinComma
import { gameAdditionalState, hideCurrentModal, miscUiState } from '../globalState'
import { options } from '../optionsStorage'
import { viewerVersionState } from '../viewerConnector'
import { lastConnectOptions } from '../appStatus'
import Chat, { Message } from './Chat'
import { useIsModalActive } from './utilsApp'
import { hideNotification, notificationProxy, showNotification } from './NotificationProvider'
import { getServerIndex, updateLoadedServerData } from './serversStorage'
import { lastConnectOptions } from './AppStatusProvider'
import { showOptionsModal } from './SelectOption'
import { withInjectableUi } from './extendableSystem'

Expand Down
Loading
Loading