Skip to content

Commit 105bfe9

Browse files
authored
Release (#458)
2 parents 4f8fb1c + 2d34ac1 commit 105bfe9

23 files changed

+206
-60
lines changed

renderer/viewer/lib/skyLight.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Calculates sky light level based on Minecraft time of day.
3+
*
4+
* Minecraft time reference:
5+
* - 0 ticks = 6:00 AM (sunrise complete)
6+
* - 6000 ticks = 12:00 PM (noon) - brightest
7+
* - 12000 ticks = 6:00 PM (sunset begins)
8+
* - 13000 ticks = 7:00 PM (dusk/night begins)
9+
* - 18000 ticks = 12:00 AM (midnight) - darkest
10+
* - 23000 ticks = 5:00 AM (dawn begins)
11+
* - 24000 ticks = 6:00 AM (same as 0)
12+
*
13+
* Sky light ranges from 4 (night) to 15 (day).
14+
*/
15+
16+
/**
17+
* Calculate celestial angle from time of day (0-1 range representing sun position)
18+
*/
19+
export const getCelestialAngle = (timeOfDay: number): number => {
20+
// Normalize time to 0-1 range
21+
let angle = ((timeOfDay % 24_000) / 24_000) - 0.25
22+
23+
if (angle < 0) angle += 1
24+
if (angle > 1) angle -= 1
25+
26+
// Vanilla Minecraft applies a smoothing curve
27+
const smoothedAngle = angle + (1 - Math.cos(angle * Math.PI)) / 2
28+
return smoothedAngle
29+
}
30+
31+
/**
32+
* Calculate sky light level (0-15) based on time of day in ticks.
33+
* Matches Minecraft vanilla behavior.
34+
*
35+
* @param timeOfDay - Time in ticks (0-24000)
36+
* @returns Sky light level (4-15, where 15 is brightest day, 4 is darkest night)
37+
*/
38+
export const calculateSkyLight = (timeOfDay: number): number => {
39+
// Normalize time to 0-24000 range
40+
const normalizedTime = ((timeOfDay % 24_000) + 24_000) % 24_000
41+
42+
// Calculate celestial angle (0-1, where 0.25 is noon, 0.75 is midnight)
43+
const celestialAngle = getCelestialAngle(normalizedTime)
44+
45+
// Calculate brightness factor based on celestial angle
46+
// cos gives us smooth day/night transition
47+
const cos = Math.cos(celestialAngle * Math.PI * 2)
48+
49+
// Map cos (-1 to 1) to brightness (0 to 1)
50+
// At noon (celestialAngle ~0.25): cos(0.5π) = 0, but we want max brightness
51+
// At midnight (celestialAngle ~0.75): cos(1.5π) = 0, but we want min brightness
52+
53+
// Vanilla-like calculation:
54+
// brightness goes from 0 (dark) to 1 (bright)
55+
const brightness = cos * 0.5 + 0.5
56+
57+
// Apply threshold - night should be darker
58+
// Vanilla has minimum sky light of 4 during night
59+
const skyLight = Math.round(4 + brightness * 11)
60+
61+
return Math.max(4, Math.min(15, skyLight))
62+
}
63+
64+
/**
65+
* Simplified sky light calculation that more closely matches vanilla behavior.
66+
* Uses piecewise linear interpolation based on known Minecraft light levels.
67+
*
68+
* @param timeOfDay - Time in ticks (0-24000)
69+
* @returns Sky light level (4-15)
70+
*/
71+
export const calculateSkyLightSimple = (timeOfDay: number): number => {
72+
// Normalize to 0-24000
73+
const time = ((timeOfDay % 24_000) + 24_000) % 24_000
74+
75+
// Vanilla Minecraft approximate sky light levels:
76+
// 0-12000 (6AM-6PM): Day, sky light = 15
77+
// 12000-13000 (6PM-7PM): Sunset transition, 15 -> 4
78+
// 13000-23000 (7PM-5AM): Night, sky light = 4
79+
// 23000-24000 (5AM-6AM): Sunrise transition, 4 -> 15
80+
81+
if (time >= 0 && time < 12_000) {
82+
// Day time - full brightness
83+
return 15
84+
} else if (time >= 12_000 && time < 13_000) {
85+
// Sunset transition (6PM to 7PM)
86+
const progress = (time - 12_000) / 1000
87+
return Math.round(15 - progress * 11)
88+
} else if (time >= 13_000 && time < 23_000) {
89+
// Night time - minimum brightness
90+
return 4
91+
} else {
92+
// Sunrise transition (5AM to 6AM)
93+
const progress = (time - 23_000) / 1000
94+
return Math.round(4 + progress * 11)
95+
}
96+
}
97+
98+
// Test/debug helper - run this to see values at different times
99+
export const debugSkyLight = () => {
100+
const testTimes = [
101+
{ ticks: 0, label: '6:00 AM (sunrise)' },
102+
{ ticks: 6000, label: '12:00 PM (noon)' },
103+
{ ticks: 12_000, label: '6:00 PM (sunset starts)' },
104+
{ ticks: 12_500, label: '6:30 PM (sunset mid)' },
105+
{ ticks: 13_000, label: '7:00 PM (night begins)' },
106+
{ ticks: 18_000, label: '12:00 AM (midnight)' },
107+
{ ticks: 19_000, label: '1:00 AM' },
108+
{ ticks: 23_000, label: '5:00 AM (dawn begins)' },
109+
{ ticks: 23_500, label: '5:30 AM (dawn mid)' },
110+
]
111+
112+
console.log('Sky Light Debug:')
113+
console.log('================')
114+
for (const { ticks, label } of testTimes) {
115+
const smooth = calculateSkyLight(ticks)
116+
const simple = calculateSkyLightSimple(ticks)
117+
console.log(`${ticks.toString().padStart(5)} ticks (${label}): smooth=${smooth}, simple=${simple}`)
118+
}
119+
}
120+
121+
// Export for global access in console
122+
if (typeof window !== 'undefined') {
123+
(window as any).debugSkyLight = debugSkyLight
124+
}

renderer/viewer/lib/worldrendererCommon.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { WorldDataEmitterWorker } from './worldDataEmitter'
1919
import { getPlayerStateUtils, PlayerStateReactive, PlayerStateRenderer, PlayerStateUtils } from './basePlayerState'
2020
import { MesherLogReader } from './mesherlogReader'
2121
import { setSkinsConfig } from './utils/skins'
22+
import { calculateSkyLightSimple } from './skyLight'
2223

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

566567
getMesherConfig (): MesherConfig {
567-
let skyLight = 15
568568
const timeOfDay = this.timeOfTheDay
569-
if (timeOfDay < 0 || timeOfDay > 24_000) {
570-
//
571-
} else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
572-
skyLight = 15
573-
} else if (timeOfDay > 6000 && timeOfDay < 12_000) {
574-
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
575-
} else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
576-
skyLight = ((timeOfDay - 12_000) / 6000) * 15
577-
}
578-
579-
skyLight = Math.floor(skyLight)
569+
const skyLight = (timeOfDay < 0 || timeOfDay > 24_000) ? 15 : calculateSkyLightSimple(timeOfDay)
580570
return {
581571
version: this.version,
582572
enableLighting: this.worldRendererConfig.enableLighting,

renderer/viewer/three/entities.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ export class Entities {
276276

277277
void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl)
278278
}
279+
this.playerEntity.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity']
279280

280281
// Update position and rotation
281282
if (playerData.position) {
@@ -294,6 +295,7 @@ export class Entities {
294295
disposeObject(mesh)
295296
}
296297
this.entities = {}
298+
this.currentSkinUrls = {}
297299

298300
// Clean up player entity
299301
if (this.playerEntity) {
@@ -481,6 +483,7 @@ export class Entities {
481483
}
482484

483485
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
486+
currentSkinUrls = {} as Record<string, string>
484487

485488
private isCanvasBlank (canvas: HTMLCanvasElement): boolean {
486489
return !canvas.getContext('2d')
@@ -517,6 +520,27 @@ export class Entities {
517520
}
518521

519522
if (typeof skinUrl !== 'string') throw new Error('Invalid skin url')
523+
524+
// Skip if same skin URL is already loaded for this entity
525+
if (this.currentSkinUrls[String(entityId)] === skinUrl) {
526+
// Still handle cape if needed
527+
if (capeUrl) {
528+
if (capeUrl === true && username) {
529+
const newCapeUrl = await loadSkinFromUsername(username, 'cape')
530+
if (!this.getPlayerObject(entityId)) return
531+
if (!newCapeUrl) return
532+
capeUrl = newCapeUrl
533+
}
534+
if (typeof capeUrl === 'string') {
535+
void this.loadAndApplyCape(entityId, capeUrl)
536+
}
537+
}
538+
return
539+
}
540+
541+
if (skinUrl !== stevePngUrl) {
542+
this.currentSkinUrls[String(entityId)] = skinUrl
543+
}
520544
const renderEars = this.worldRenderer.worldRendererConfig.renderEars || username === 'deadmau5'
521545
void this.loadAndApplySkin(entityId, skinUrl, renderEars).then(async () => {
522546
if (capeUrl) {
@@ -575,6 +599,7 @@ export class Entities {
575599
skinTexture.needsUpdate = true
576600
playerObject.skin.map = skinTexture as any
577601
playerObject.skin.modelType = inferModelType(skinCanvas)
602+
playerObject.skin['isCustom'] = skinUrl !== stevePngUrl
578603

579604
let earsCanvas: HTMLCanvasElement | undefined
580605
if (!playerCustomSkinImage) {
@@ -1155,6 +1180,7 @@ export class Entities {
11551180
playerPerAnimation = {} as Record<number, string>
11561181
onRemoveEntity (entity: import('prismarine-entity').Entity) {
11571182
this.loadedSkinEntityIds.delete(entity.id.toString())
1183+
delete this.currentSkinUrls[entity.id.toString()]
11581184
}
11591185

11601186
updateMap (mapNumber: string | number, data: string) {

renderer/viewer/three/waypoints.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface Waypoint {
88
y: number
99
z: number
1010
minDistance: number
11+
maxDistance: number
1112
color: number
1213
label?: string
1314
sprite: WaypointSprite
@@ -17,6 +18,7 @@ interface WaypointOptions {
1718
color?: number
1819
label?: string
1920
minDistance?: number
21+
maxDistance?: number
2022
metadata?: any
2123
}
2224

@@ -54,7 +56,8 @@ export class WaypointsRenderer {
5456
for (const waypoint of this.waypoints.values()) {
5557
const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
5658
const distance = playerPos.distanceTo(waypointPos)
57-
const visible = !waypoint.minDistance || distance >= waypoint.minDistance
59+
const visible = (!waypoint.minDistance || distance >= waypoint.minDistance) &&
60+
(waypoint.maxDistance === Infinity || distance <= waypoint.maxDistance)
5861

5962
waypoint.sprite.setVisible(visible)
6063

@@ -92,6 +95,7 @@ export class WaypointsRenderer {
9295
const color = options.color ?? 0xFF_00_00
9396
const { label, metadata } = options
9497
const minDistance = options.minDistance ?? 0
98+
const maxDistance = options.maxDistance ?? Infinity
9599

96100
const sprite = createWaypointSprite({
97101
position: new THREE.Vector3(x, y, z),
@@ -105,7 +109,7 @@ export class WaypointsRenderer {
105109
this.waypointScene.add(sprite.group)
106110

107111
this.waypoints.set(id, {
108-
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
112+
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance, maxDistance,
109113
color, label,
110114
sprite,
111115
})

src/appStatus.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { resetStateAfterDisconnect } from './browserfs'
2+
import type { ConnectOptions } from './connect'
23
import { hideModal, activeModalStack, showModal, miscUiState } from './globalState'
34
import { appStatusState, resetAppStatusState } from './react/AppStatusProvider'
45

@@ -39,3 +40,9 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
3940
}
4041
}
4142
globalThis.setLoadingScreenStatus = setLoadingScreenStatus
43+
44+
export const lastConnectOptions = {
45+
value: null as ConnectOptions | null,
46+
hadWorldLoaded: false
47+
}
48+
globalThis.lastConnectOptions = lastConnectOptions

src/controls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { getItemFromBlock } from './chatUtils'
2222
import { gamepadUiCursorState } from './react/GamepadUiCursor'
2323
import { completeResourcepackPackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack'
2424
import { showNotification } from './react/NotificationProvider'
25-
import { lastConnectOptions } from './react/AppStatusProvider'
25+
import { lastConnectOptions } from './appStatus'
2626
import { onCameraMove, onControInit } from './cameraRotationControls'
2727
import { createNotificationProgressReporter } from './core/progressReporter'
2828
import { appStorage } from './react/appStorageProvider'

src/customChannels.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import PItem from 'prismarine-item'
22
import * as THREE from 'three'
33
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
4-
import { options } from './optionsStorage'
4+
import { options, serverChangedSettings } from './optionsStorage'
55
import { jeiCustomCategories } from './inventoryWindows'
66
import { registerIdeChannels } from './core/ideChannels'
77
import { serverSafeSettings } from './defaultOptions'
8+
import { lastConnectOptions } from './appStatus'
9+
10+
const isWebSocketServer = (server: string | undefined) => {
11+
if (!server) return false
12+
return server.startsWith('ws://') || server.startsWith('wss://')
13+
}
14+
15+
const getIsCustomChannelsEnabled = () => {
16+
if (options.customChannels === 'websocket') return isWebSocketServer(lastConnectOptions.value?.server)
17+
return options.customChannels
18+
}
819

920
export default () => {
1021
customEvents.on('mineflayerBotCreated', async () => {
11-
if (!options.customChannels) return
22+
if (!getIsCustomChannelsEnabled()) return
1223
bot.once('login', () => {
1324
registerBlockModelsChannel()
1425
registerMediaChannels()
@@ -148,6 +159,7 @@ const registerWaypointChannels = () => {
148159

149160
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
150161
minDistance: data.minDistance,
162+
maxDistance: metadata.maxDistance,
151163
label: data.label || undefined,
152164
color: data.color || undefined,
153165
metadata
@@ -566,17 +578,8 @@ const registerServerSettingsChannel = () => {
566578
continue
567579
}
568580

569-
// Validate type matches
570-
const currentValue = options[key]
571-
572-
// For union types, check if value is valid
573-
if (Array.isArray(currentValue) && !Array.isArray(value)) {
574-
console.warn(`Type mismatch for setting ${key}: expected array`)
575-
skippedCount++
576-
continue
577-
}
578-
579-
// Apply the setting
581+
// todo remove it later, let user take control back and make clear to user
582+
serverChangedSettings.value.add(key)
580583
options[key] = value
581584
appliedCount++
582585
}

src/defaultOptions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const defaultOptions = {
5151
displayRecordButton: true,
5252
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
5353
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
54-
customChannels: false,
54+
customChannels: 'websocket' as boolean | 'websocket',
5555
remoteContentNotSameOrigin: false as boolean | string[],
5656
packetsRecordingAutoStart: false,
5757
language: 'auto',
@@ -165,6 +165,7 @@ function getTouchControlsSize () {
165165
* Settings like modsSupport, customChannels, or security-related options are excluded.
166166
*/
167167
export const serverSafeSettings: Partial<Record<keyof typeof defaultOptions, true>> = {
168+
remoteContentNotSameOrigin: true, // allow server to change remote content not same origin policy
168169
renderEars: true,
169170
viewBobbing: true,
170171
mouseRawInput: true,

src/entities.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,10 @@ customEvents.on('gameLoaded', () => {
237237
const skinUrl = applySkinTexturesProxy(player.skinData.url)
238238
const capeUrl = applySkinTexturesProxy((player.skinData as any).capeUrl)
239239

240-
// Find entity with matching UUID and update skin
241-
let entityId = ''
242-
for (const [entId, entity] of Object.entries(bot.entities)) {
243-
if (entity.uuid === player.uuid) {
244-
entityId = entId
245-
break
246-
}
240+
void getThreeJsRendererMethods()!.updatePlayerSkin(player.entity?.id ?? '', player.username, player.uuid, skinUrl ?? true, capeUrl)
241+
if (player.entity === bot.entity) {
242+
appViewer.playerState.reactive.playerSkin = skinUrl
247243
}
248-
// even if not found, still record to cache
249-
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
250244
} catch (err) {
251245
reportError(new Error('Error applying skin texture:', { cause: err }))
252246
}

0 commit comments

Comments
 (0)