This commit is contained in:
Vitaly 2024-02-19 03:20:46 +03:00 committed by GitHub
commit f58eb01c79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2795 additions and 234 deletions

View file

@ -8,6 +8,9 @@ After forking the repository, run the following commands to get started:
A few notes:
- Use `next` branch for development and as base & target branch for pull requests if possible.
- To link dependency locally e.g. flying-squid add this to `pnpm` > `overrides` of root package.json: `"flying-squid": "file:../space-squid",` (with some modules `pnpm link` also works)
- It's recommended to use debugger for debugging. VSCode has a great debugger built-in. If debugger is slow, you can use `--no-sources` flag that would allow browser to speedup .map file parsing.
- Some data are cached between restarts. If you see something doesn't work after upgrading dependencies, try to clear the by simply removing the `dist` folder.
- The same folder `dist` is used for both development and production builds, so be careful when deploying the project.

BIN
assets/generic_91.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/generic_92.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/generic_93.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/generic_94.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/generic_95.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -3,5 +3,5 @@
"defaultHost": "<from-proxy>",
"defaultProxy": "zardoy.site:2344",
"defaultVersion": "1.18.2",
"mapsProvider": "zardoy.site/maps"
"mapsProvider": "https://maps.mcraft.fun/"
}

View file

@ -70,7 +70,8 @@ const buildOptions = {
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`)
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`),
'process.env.DEPS_VERSIONS': JSON.stringify({})
},
loader: {
// todo use external or resolve issues with duplicating

View file

@ -12,8 +12,8 @@
<div style="position: fixed;inset: 0;z-index: -1;background-color: rgba(0, 0, 0, 0.8);"></div>
</div>
<div>
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;">Loading...</div>
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);">A true Minecraft client in your browser!</div>
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;" class="title">Loading...</div>
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);" class="subtitle">A true Minecraft client in your browser!</div>
</div>
</div>
`
@ -22,6 +22,17 @@
if (!window.pageLoaded) {
document.documentElement.appendChild(loadingDivElem)
}
// load error handling
const onError = (message) => {
console.log(message)
if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') {
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = message
window.location.hash = '#dev' // show eruda
}
}
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
window.addEventListener('error', (e) => onError(e.message))
</script>
<script type="module" async>
const checkLoadEruda = () => {

View file

@ -53,7 +53,7 @@
"lit": "^2.8.0",
"lodash-es": "^4.17.21",
"minecraft-assets": "^1.12.2",
"minecraft-data": "3.60.0",
"minecraft-data": "3.61.0",
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
"node-gzip": "^1.1.2",
"peerjs": "^1.5.0",
@ -129,7 +129,7 @@
"diamond-square": "github:zardoy/diamond-square",
"prismarine-block": "github:zardoy/prismarine-block#next-era",
"prismarine-world": "github:zardoy/prismarine-world#next-era",
"minecraft-data": "3.60.0",
"minecraft-data": "3.61.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"minecraft-protocol": "github:zardoy/minecraft-protocol#everything",
"react": "^18.2.0"

565
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,8 @@ import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToC
import stevePng from 'minecraft-assets/minecraft-assets/data/1.20.2/entity/player/wide/steve.png'
import { WalkingGeneralSwing } from './entity/animations'
import { NameTagObject } from 'skinview3d/libs/nametag'
import { fromFormattedString } from '@xmcl/text-component'
import { flat, fromFormattedString } from '@xmcl/text-component'
import mojangson from 'mojangson'
export const TWEEN_DURATION = 50 // todo should be 100
@ -39,6 +40,23 @@ function getUsernameTexture (username, { fontFamily = 'sans-serif' }) {
return canvas
}
const addNametag = (entity, options, mesh) => {
if (entity.username !== undefined) {
if (mesh.children.find(c => c.name === 'nametag')) return // todo update
const canvas = getUsernameTexture(entity.username, options)
const tex = new THREE.Texture(canvas)
tex.needsUpdate = true
const spriteMat = new THREE.SpriteMaterial({ map: tex })
const sprite = new THREE.Sprite(spriteMat)
sprite.renderOrder = 1000
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
sprite.position.y += entity.height + 0.6
sprite.name = 'nametag'
mesh.add(sprite)
}
}
function getEntityMesh (entity, scene, options, overrides) {
if (entity.name) {
try {
@ -46,18 +64,7 @@ function getEntityMesh (entity, scene, options, overrides) {
const entityName = entity.name.toLowerCase()
const e = new Entity('1.16.4', entityName, scene, overrides)
if (entity.username !== undefined) {
const canvas = getUsernameTexture(entity.username, options)
const tex = new THREE.Texture(canvas)
tex.needsUpdate = true
const spriteMat = new THREE.SpriteMaterial({ map: tex })
const sprite = new THREE.Sprite(spriteMat)
sprite.renderOrder = 1000
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
sprite.position.y += entity.height + 0.6
e.mesh.add(sprite)
}
addNametag(entity, options, e.mesh)
return e.mesh
} catch (err) {
console.log(err)
@ -228,6 +235,13 @@ export class Entities extends EventEmitter {
}
displaySimpleText (jsonLike) {
if (!jsonLike) return
const parsed = mojangson.simplify(mojangson.parse(jsonLike))
const text = flat(parsed).map(x => x.text)
return text.join('')
}
update (/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
if (!this.entities[entity.id] && !entity.delete) {
const group = new THREE.Group()
@ -295,6 +309,21 @@ export class Entities extends EventEmitter {
this.setVisible(this.visible, group)
}
//@ts-ignore
const isInvisible = entity.metadata?.[0] & 0x20
if (isInvisible) {
for (const child of this.entities[entity.id].children.find(c => c.name === 'mesh').children) {
if (child.name !== 'nametag') {
child.visible = false
}
}
}
// not player
const displayText = entity.metadata?.[3] && this.displaySimpleText(entity.metadata[2]);
if (entity.name !== 'player') {
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
}
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
const e = this.entities[entity.id]

View file

@ -259,7 +259,7 @@ export class WorldRenderer {
const loadBlockStates = async () => {
return new Promise(resolve => {
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
return loadJSON(`blocksStates/${this.texturesVersion}.json`, (data) => {
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
this.downloadedBlockStatesData = data
// todo
this.renderUpdateEmitter.emit('blockStatesDownloaded')

View file

@ -1,5 +1,6 @@
import { options } from './optionsStorage'
import { isCypress } from './standaloneUtils'
import { reportWarningOnce } from './utils'
let audioContext: AudioContext
const sounds: Record<string, any> = {}
@ -39,7 +40,12 @@ export async function playSound (url, soundVolume = 1) {
if (!volume) return
audioContext ??= new window.AudioContext()
try {
audioContext ??= new window.AudioContext()
} catch (err) {
reportWarningOnce('audioContext', 'Failed to create audio context. Some sounds will not play')
return
}
for (const [soundName, sound] of Object.entries(sounds)) {
if (convertedSounds.includes(soundName)) continue

View file

@ -114,6 +114,6 @@ const blockToItemRemaps = {
}
export const getItemFromBlock = (block: import('prismarine-block').Block) => {
const item = loadedData.blocks[blockToItemRemaps[block.name] ?? block.name]
const item = loadedData.items[blockToItemRemaps[block.name] ?? block.name]
return item
}

View file

@ -68,6 +68,7 @@ export const contro = new ControMax({
},
gamepadPollingInterval: 10
})
window.controMax = contro
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
const setSprinting = (state: boolean) => {
@ -414,12 +415,12 @@ let allowFlying = false
export const onBotCreate = () => {
bot._client.on('abilities', ({ flags }) => {
allowFlying = !!(flags & 4)
if (flags & 2) { // flying
toggleFly(true, false)
} else {
toggleFly(false, false)
}
allowFlying = !!(flags & 4)
})
}

View file

@ -6,9 +6,9 @@ export default () => {
assertDefined(viewer)
// 0 morning
const dayTotal = 24_000
const evening = 12_542
const night = 17_843
const morningStart = 22_300
const evening = 11_500
const night = 13_500
const morningStart = 23_000
const morningEnd = 23_961
const timeProgress = options.dayCycleAndLighting ? bot.time.timeOfDay : 0

17
src/gameUnload.ts Normal file
View file

@ -0,0 +1,17 @@
import { subscribe } from 'valtio'
import { miscUiState } from './globalState'
let toCleanup = [] as Array<() => void>
export const watchUnloadForCleanup = (func: () => void) => {
toCleanup.push(func)
}
subscribe(miscUiState, () => {
if (!miscUiState.gameLoaded) {
for (const func of toCleanup) {
func()
}
toCleanup = []
}
})

View file

@ -0,0 +1,543 @@
export interface ClientWriteMap {
keep_alive: /** 1.7 */ {
keepAliveId: number;
} | /** 1.12.2 */ {
keepAliveId: bigint;
};
/** Removed in 1.19 */
chat: /** 1.7 */ {
message: string;
};
use_entity: /** 1.7 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
} | /** 1.9 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
hand: any;
} | /** 1.16 */ {
target: number;
mouse: number;
x: any;
y: any;
z: any;
hand: any;
sneaking: boolean;
};
flying: /** 1.7 */ {
onGround: boolean;
};
position: /** 1.7 */ {
x: number;
stance: number;
y: number;
z: number;
onGround: boolean;
} | /** 1.8 */ {
x: number;
y: number;
z: number;
onGround: boolean;
};
look: /** 1.7 */ {
yaw: number;
pitch: number;
onGround: boolean;
};
position_look: /** 1.7 */ {
x: number;
stance: number;
y: number;
z: number;
yaw: number;
pitch: number;
onGround: boolean;
} | /** 1.8 */ {
x: number;
y: number;
z: number;
yaw: number;
pitch: number;
onGround: boolean;
};
block_dig: /** 1.7 */ {
status: number;
location: any;
face: number;
} | /** 1.8 */ {
status: number;
location: { x: number, y: number, z: number };
face: number;
} | /** 1.19 */ {
status: number;
location: { x: number, y: number, z: number };
face: number;
sequence: number;
};
block_place: /** 1.7 */ {
location: any;
direction: number;
heldItem: any;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.8 */ {
location: { x: number, y: number, z: number };
direction: number;
heldItem: any;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.9 */ {
location: { x: number, y: number, z: number };
direction: number;
hand: number;
cursorX: number;
cursorY: number;
cursorZ: number;
} | /** 1.14 */ {
hand: number;
location: { x: number, y: number, z: number };
direction: number;
cursorX: number;
cursorY: number;
cursorZ: number;
insideBlock: boolean;
} | /** 1.19 */ {
hand: number;
location: { x: number, y: number, z: number };
direction: number;
cursorX: number;
cursorY: number;
cursorZ: number;
insideBlock: boolean;
sequence: number;
};
held_item_slot: /** 1.7 */ {
slotId: number;
};
arm_animation: /** 1.7 */ {
entityId: number;
animation: number;
} | /** 1.8 */ {
} | /** 1.9 */ {
hand: number;
};
entity_action: /** 1.7 */ {
entityId: number;
actionId: number;
jumpBoost: number;
};
steer_vehicle: /** 1.7 */ {
sideways: number;
forward: number;
jump: boolean;
unmount: boolean;
} | /** 1.8 */ {
sideways: number;
forward: number;
jump: number;
};
close_window: /** 1.7 */ {
windowId: number;
};
window_click: /** 1.7 */ {
windowId: number;
slot: number;
mouseButton: number;
action: number;
mode: number;
item: any;
} | /** 1.17 */ {
windowId: number;
slot: number;
mouseButton: number;
mode: number;
changedSlots: any;
cursorItem: any;
} | /** 1.17.1 */ {
windowId: number;
stateId: number;
slot: number;
mouseButton: number;
mode: number;
changedSlots: any;
cursorItem: any;
};
/** Removed in 1.17 */
transaction: /** 1.7 */ {
windowId: number;
action: number;
accepted: boolean;
};
set_creative_slot: /** 1.7 */ {
slot: number;
item: any;
};
enchant_item: /** 1.7 */ {
windowId: number;
enchantment: number;
};
update_sign: /** 1.7 */ {
location: any;
text1: string;
text2: string;
text3: string;
text4: string;
} | /** 1.8 */ {
location: { x: number, y: number, z: number };
text1: string;
text2: string;
text3: string;
text4: string;
} | /** 1.20 */ {
location: { x: number, y: number, z: number };
isFrontText: boolean;
text1: string;
text2: string;
text3: string;
text4: string;
};
abilities: /** 1.7 */ {
flags: number;
flyingSpeed: number;
walkingSpeed: number;
} | /** 1.16 */ {
flags: number;
};
tab_complete: /** 1.7 */ {
text: string;
} | /** 1.8 */ {
text: string;
block: any;
} | /** 1.9 */ {
text: string;
assumeCommand: boolean;
lookedAtBlock: any;
} | /** 1.13 */ {
transactionId: number;
text: string;
};
settings: /** 1.7 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
difficulty: number;
showCape: boolean;
} | /** 1.8 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
} | /** 1.9 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
} | /** 1.17 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
disableTextFiltering: boolean;
} | /** 1.18 */ {
locale: string;
viewDistance: number;
chatFlags: number;
chatColors: boolean;
skinParts: number;
mainHand: number;
enableTextFiltering: boolean;
enableServerListing: boolean;
};
client_command: /** 1.7 */ {
payload: number;
} | /** 1.9 */ {
actionId: number;
};
custom_payload: /** 1.7 */ {
channel: string;
data: any;
};
packet: /** 1.7 */ {
name: any;
params: any;
};
spectate: /** 1.8 */ {
target: any;
};
resource_pack_receive: /** 1.8 */ {
hash: string;
result: number;
} | /** 1.10 */ {
result: number;
} | /** 1.20.3 */ {
uuid: any;
result: number;
};
teleport_confirm: /** 1.9 */ {
teleportId: number;
};
vehicle_move: /** 1.9 */ {
x: number;
y: number;
z: number;
yaw: number;
pitch: number;
};
steer_boat: /** 1.9 */ {
leftPaddle: boolean;
rightPaddle: boolean;
};
use_item: /** 1.9 */ {
hand: number;
} | /** 1.19 */ {
hand: number;
sequence: number;
};
/** Removed in 1.12.1 */
prepare_crafting_grid: /** 1.12 */ {
windowId: number;
actionNumber: number;
returnEntry: any;
prepareEntry: any;
};
/** Removed in 1.16.2 */
crafting_book_data: /** 1.12 */ {
type: number;
undefined: any;
};
advancement_tab: /** 1.12 */ {
action: number;
tabId: any;
};
craft_recipe_request: /** 1.12.1 */ {
windowId: number;
recipe: number;
makeAll: boolean;
} | /** 1.13 */ {
windowId: number;
recipe: string;
makeAll: boolean;
};
query_block_nbt: /** 1.13 */ {
transactionId: number;
location: { x: number, y: number, z: number };
};
edit_book: /** 1.13 */ {
new_book: any;
signing: boolean;
} | /** 1.13.1 */ {
new_book: any;
signing: boolean;
hand: number;
} | /** 1.17.1 */ {
hand: number;
pages: any;
title: any;
};
query_entity_nbt: /** 1.13 */ {
transactionId: number;
entityId: number;
};
pick_item: /** 1.13 */ {
slot: number;
};
name_item: /** 1.13 */ {
name: string;
};
select_trade: /** 1.13 */ {
slot: number;
};
set_beacon_effect: /** 1.13 */ {
primary_effect: number;
secondary_effect: number;
} | /** 1.19 */ {
primary_effect: any;
secondary_effect: any;
};
update_command_block: /** 1.13 */ {
location: { x: number, y: number, z: number };
command: string;
mode: number;
flags: number;
};
update_command_block_minecart: /** 1.13 */ {
entityId: number;
command: string;
track_output: boolean;
};
update_structure_block: /** 1.13 */ {
location: { x: number, y: number, z: number };
action: number;
mode: number;
name: string;
offset_x: number;
offset_y: number;
offset_z: number;
size_x: number;
size_y: number;
size_z: number;
mirror: number;
rotation: number;
metadata: string;
integrity: number;
seed: any;
flags: number;
} | /** 1.19 */ {
location: { x: number, y: number, z: number };
action: number;
mode: number;
name: string;
offset_x: number;
offset_y: number;
offset_z: number;
size_x: number;
size_y: number;
size_z: number;
mirror: number;
rotation: number;
metadata: string;
integrity: number;
seed: number;
flags: number;
};
set_difficulty: /** 1.14 */ {
newDifficulty: number;
};
lock_difficulty: /** 1.14 */ {
locked: boolean;
};
update_jigsaw_block: /** 1.14 */ {
location: { x: number, y: number, z: number };
attachmentType: string;
targetPool: string;
finalState: string;
} | /** 1.16 */ {
location: { x: number, y: number, z: number };
name: string;
target: string;
pool: string;
finalState: string;
jointType: string;
} | /** 1.20.3 */ {
location: { x: number, y: number, z: number };
name: string;
target: string;
pool: string;
finalState: string;
jointType: string;
selection_priority: number;
placement_priority: number;
};
generate_structure: /** 1.16 */ {
location: { x: number, y: number, z: number };
levels: number;
keepJigsaws: boolean;
};
displayed_recipe: /** 1.16.2 */ {
recipeId: string;
};
recipe_book: /** 1.16.2 */ {
bookId: number;
bookOpen: boolean;
filterActive: boolean;
};
pong: /** 1.17 */ {
id: number;
};
chat_command: /** 1.19 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
signedPreview: boolean;
} | /** 1.19.2 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
signedPreview: boolean;
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
command: string;
timestamp: bigint;
salt: bigint;
argumentSignatures: any;
messageCount: number;
acknowledged: any;
};
chat_message: /** 1.19 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
signedPreview: boolean;
} | /** 1.19.2 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
signedPreview: boolean;
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
message: string;
timestamp: bigint;
salt: bigint;
signature: any;
offset: number;
acknowledged: any;
};
/** Removed in 1.19.3 */
chat_preview: /** 1.19 */ {
query: number;
message: string;
};
message_acknowledgement: /** 1.19.2 */ {
previousMessages: any;
lastRejectedMessage: any;
} | /** 1.19.3 */ {
count: number;
};
chat_session_update: /** 1.19.3 */ {
sessionUUID: any;
expireTime: bigint;
publicKey: any;
signature: any;
};
chunk_batch_received: /** 1.20.2 */ {
chunksPerTick: number;
};
/** Removed in 1.20.3 */
configuation_acknowledged: /** 1.20.2 */ {
};
ping_request: /** 1.20.2 */ {
id: bigint;
};
configuration_acknowledged: /** 1.20.3 */ {
};
set_slot_state: /** 1.20.3 */ {
slot_id: number;
window_id: number;
state: boolean;
};
}
export declare const clientWrite: <T extends keyof ClientWriteMap>(name: T, data: ClientWriteMap[T]) => Buffer

File diff suppressed because it is too large Load diff

15
src/globals.d.ts vendored
View file

@ -2,7 +2,12 @@
declare const THREE: typeof import('three')
// todo make optional
declare const bot: Omit<import('mineflayer').Bot, 'world'> & { world: import('prismarine-world').world.WorldSync }
declare const bot: Omit<import('mineflayer').Bot, 'world' | '_client'> & {
world: import('prismarine-world').world.WorldSync
_client: import('minecraft-protocol').Client & {
write: typeof import('./generatedClientPackets').clientWrite
}
}
declare const __type_bot: typeof bot
declare const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer
declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined
@ -66,3 +71,11 @@ declare module '*.png' {
const png: string
export default png
}
interface PromiseConstructor {
withResolvers<T> (): {
resolve: (value: T) => void;
reject: (reason: any) => void;
promise: Promise<T>;
}
}

View file

@ -23,6 +23,8 @@ import './menus/hud'
import './menus/play_screen'
import './menus/pause_screen'
import './menus/keybinds_screen'
import 'core-js/features/array/at'
import 'core-js/features/promise/with-resolvers'
import { initWithRenderer, statsEnd, statsStart } from './topRightStats'
import PrismarineBlock from 'prismarine-block'
@ -88,9 +90,11 @@ import { loadInMemorySave } from './react/SingleplayerProvider'
// side effects
import { downloadSoundsIfNeeded } from './soundSystem'
import { ua } from './react/utils'
window.debug = debug
window.THREE = THREE
window.worldInteractions = worldInteractions
window.beforeRenderFrame = []
// ACTUAL CODE
@ -100,16 +104,32 @@ watchFov()
initCollisionShapes()
// Create three.js context, add to page
const renderer = new THREE.WebGLRenderer({
powerPreference: options.gpuPreference,
})
let renderer: THREE.WebGLRenderer
try {
renderer = new THREE.WebGLRenderer({
powerPreference: options.gpuPreference,
})
} catch (err) {
console.error(err)
throw new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)
}
// renderer.localClippingEnabled = true
initWithRenderer(renderer.domElement)
window.renderer = renderer
renderer.setPixelRatio(window.devicePixelRatio || 1) // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
if (!renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
renderer.setPixelRatio(pixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.domElement.id = 'viewer-canvas'
document.body.appendChild(renderer.domElement)
const isFirefox = ua.getBrowser().name === 'Firefox'
if (isFirefox) {
// set custom property
document.body.style.setProperty('--thin-if-firefox', 'thin')
}
// Create viewer
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers)
window.viewer = viewer

View file

@ -93,11 +93,15 @@ class HealthBar extends LitElement {
}
effectAdded (effect) {
this.shadowRoot.querySelector('#health').classList.add(getEffectClass(effect))
const effectClass = getEffectClass(effect)
if (!effectClass) return
this.shadowRoot.querySelector('#health').classList.add(effectClass)
}
effectEnded (effect) {
this.shadowRoot.querySelector('#health').classList.remove(getEffectClass(effect))
const effectClass = getEffectClass(effect)
if (!effectClass) return
this.shadowRoot.querySelector('#health').classList.remove(effectClass)
}
onDamage () {

View file

@ -4,7 +4,8 @@ const { subscribe } = require('valtio')
const { subscribeKey } = require('valtio/utils')
const { hideCurrentModal, showModal, miscUiState, notification, openOptionsMenu } = require('../globalState')
const { fsState } = require('../loadSave')
const { disconnect, openGithub } = require('../utils')
const { openGithub } = require('../utils')
const { disconnect } = require('../flyingSquidUtils')
const { closeWan, openToWanAndCopyJoinLink, getJoinLink } = require('../localServerMultiplayer')
const { uniqueFileNameFromWorldName, copyFilesAsyncWithProgress } = require('../browserfs')
const { showOptionsModal } = require('../react/SelectOption')

View file

@ -29,6 +29,7 @@ const defaultOptions = {
touchButtonsSize: 40,
touchButtonsOpacity: 80,
touchButtonsPosition: 12,
touchControlsPositions: {} as Record<string, [number, number]>,
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
/** @unstable */
disableAssets: false,

View file

@ -6,6 +6,12 @@ import LargeChestLikeGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui
import FurnaceGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/furnace.png'
import CraftingTableGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/crafting_table.png'
import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/dispenser.png'
import HopperGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/hopper.png'
import HorseGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/horse.png'
import VillagerGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/villager2.png'
import EnchantingGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/enchanting_table.png'
import AnvilGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/anvil.png'
import BeaconGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/container/beacon.png'
import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png'
import { subscribeKey } from 'valtio/utils'
@ -22,6 +28,7 @@ import mojangson from 'mojangson'
import nbt from 'prismarine-nbt'
import { splitEvery, equals } from 'rambda'
import PItem, { Item } from 'prismarine-item'
import Generic95 from '../assets/generic_95.png'
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState'
import invspriteJson from './invsprite.json'
import { options } from './optionsStorage'
@ -184,6 +191,13 @@ const getImageSrc = (path): string | HTMLImageElement => {
case 'gui/container/crafting_table': return CraftingTableGui
case 'gui/container/shulker_box': return ChestLikeGui
case 'gui/container/generic_54': return LargeChestLikeGui
case 'gui/container/generic_95': return Generic95
case 'gui/container/hopper': return HopperGui
case 'gui/container/horse': return HorseGui
case 'gui/container/villager2': return VillagerGui
case 'gui/container/enchanting_table': return EnchantingGui
case 'gui/container/anvil': return AnvilGui
case 'gui/container/beacon': return BeaconGui
}
return Dirt
}
@ -228,7 +242,7 @@ const isFullBlock = (block: string) => {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
type RenderSlot = Pick<import('prismarine-item').Item, 'name' | 'displayName'>
type RenderSlot = Pick<import('prismarine-item').Item, 'name' | 'displayName' | 'durabilityUsed' | 'maxDurability' | 'enchants'>
const renderSlot = (slot: RenderSlot, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
const itemName = slot.name
const isItem = loadedData.itemsByName[itemName]
@ -332,11 +346,21 @@ export const onModalClose = (callback: () => any) => {
const implementedContainersGuiMap = {
// todo allow arbitrary size instead!
'minecraft:generic_9x3': 'ChestWin',
'minecraft:generic_9x5': 'Generic95Win',
// hopper
'minecraft:generic_5x1': 'HopperWin',
'minecraft:generic_9x6': 'LargeChestWin',
'minecraft:generic_3x3': 'DropDispenseWin',
'minecraft:furnace': 'FurnaceWin',
'minecraft:smoker': 'FurnaceWin',
'minecraft:crafting': 'CraftingWin'
'minecraft:crafting': 'CraftingWin',
'minecraft:anvil': 'AnvilWin',
// enchant
'minecraft:enchanting_table': 'EnchantingWin',
// horse
'minecraft:horse': 'HorseWin',
// villager
'minecraft:villager': 'VillagerWin',
}
const upJei = (search: string) => {

View file

@ -95,6 +95,7 @@ type Story = StoryObj<typeof Chat>
export const Primary: Story = {
args: {
usingTouch: false,
messages: [{
parts: [
{

View file

@ -122,7 +122,7 @@ div.chat-wrapper {
pointer-events: none;
overflow: hidden;
width: 100%;
scrollbar-width: thin;
scrollbar-width: var(--thin-if-firefox);
}
.chat.opened {

View file

@ -29,6 +29,7 @@ const MessageLine = ({ message }: { message: Message }) => {
type Props = {
messages: Message[]
usingTouch: boolean
opacity?: number
opened?: boolean
onClose?: () => void
@ -52,9 +53,7 @@ export const fadeMessage = (message: Message, initialTimeout: boolean, requestUp
}, initialTimeout ? 5000 : 0)
}
export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessage, onClose }: Props) => {
const usingTouch = useSnapshot(miscUiState).currentTouch
export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessage, onClose, usingTouch }: Props) => {
const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]'))
const [completePadText, setCompletePadText] = useState('')

View file

@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { useSnapshot } from 'valtio'
import { formatMessage } from '../botUtils'
import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands'
import { hideCurrentModal } from '../globalState'
import { hideCurrentModal, miscUiState } from '../globalState'
import { options } from '../optionsStorage'
import ChatContainer, { Message, fadeMessage } from './ChatContainer'
import { useIsModalActive } from './utils'
@ -11,6 +12,7 @@ export default () => {
const isChatActive = useIsModalActive('chat')
const { messagesLimit, chatOpacity, chatOpacityOpened } = options
const lastMessageId = useRef(0)
const usingTouch = useSnapshot(miscUiState).currentTouch
useEffect(() => {
bot.addListener('message', (jsonMsg, position) => {
@ -33,6 +35,7 @@ export default () => {
}, [])
return <ChatContainer
usingTouch={!!usingTouch}
opacity={(isChatActive ? chatOpacityOpened : chatOpacity) / 100}
messages={messages}
opened={isChatActive}

View file

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react'
import Button from './Button'
const defaultIcon = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 21H5H19H21V3H19H5H3V21ZM19 5V19H5V5H19ZM11 17H13V11H15V9H13V7H11V9H9V11H11V17ZM9 13V11H7V13H9ZM17 13H15V11H17V13Z" fill="currentColor"></path></svg>
const Button2 = ({ title, icon }) => {
//@ts-expect-error
return <Button style={{ '--scale': 4 }}>
<div style={{ fontSize: '22px', fontWeight: 'bold', display: 'flex', gap: 3, flexDirection: 'column', alignItems: 'center' }}>
<div>
{title}
</div>
{/* <iconify-icon icon="pixelarticons: */}
<div style={{ width: 30, height: 30 }} className='full-svg'>
{icon}
</div>
</div>
</Button>
}
const Comp = () => {
return <div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 10
}}>
<Button2 title="/give" icon={defaultIcon} />
<Button2 title="/tell" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 2h18v16H6v2H4v-2h2v-2h14V4H4v18H2V2h2zm5 7H7v2h2V9zm2 0h2v2h-2V9zm6 0h-2v2h2V9z" fill="currentColor" /> </svg>} />
<Button2 title="/setblock" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M2 2h20v20H2V2zm2 2v4h4V4H4zm6 0v4h4V4h-4zm6 0v4h4V4h-4zm4 6h-4v4h4v-4zm0 6h-4v4h4v-4zm-6 4v-4h-4v4h4zm-6 0v-4H4v4h4zm-4-6h4v-4H4v4zm6-4v4h4v-4h-4z" fill="currentColor" /> </svg>} />
<Button2 title="/tp" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16 5H2v14h14v-2h2v-2h2v-2h2v-2h-2V9h-2V7h-2V5zm0 2v2h2v2h2v2h-2v2h-2v2H4V7h12z" fill="currentColor" /> </svg>} />
<Button2 title="/clone" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M5 3H3v2h2V3zm2 4h2v2H7V7zm4 0h2v2h-2V7zm2 12h-2v2h2v-2zm2 0h2v2h-2v-2zm6 0h-2v2h2v-2zM7 11h2v2H7v-2zm14 0h-2v2h2v-2zm-2 4h2v2h-2v-2zM7 19h2v2H7v-2zM19 7h2v2h-2V7zM7 3h2v2H7V3zm2 12H7v2h2v-2zM3 7h2v2H3V7zm14 0h-2v2h2V7zM3 11h2v2H3v-2zm2 4H3v2h2v-2zm6-12h2v2h-2V3zm6 0h-2v2h2V3z" fill="currentColor" /> </svg>} />
<Button2 title="/fill" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M21 3h-8v2h4v2h2v4h2V3zm-4 4h-2v2h-2v2h2V9h2V7zm-8 8h2v-2H9v2H7v2h2v-2zm-4-2v4h2v2H5h6v2H3v-8h2z" fill="currentColor" /> </svg>} />
<Button2 title="/home" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M14 2h-4v2H8v2H6v2H4v2H2v2h2v10h7v-6h2v6h7V12h2v-2h-2V8h-2V6h-2V4h-2V2zm0 2v2h2v2h2v2h2v2h-2v8h-3v-6H9v6H6v-8H4v-2h2V8h2V6h2V4h4z" fill="currentColor" /> </svg>} />
<Button2 title="/time" icon={<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path d="M20 0h2v2h2v2h-2v2h-2V4h-2V2h2V0ZM8 4h8v2h-2v2h-2V6H8V4ZM6 8V6h2v2H6Zm0 8H4V8h2v8Zm2 2H6v-2h2v2Zm8 0v2H8v-2h8Zm2-2v2h-2v-2h2Zm-2-4v-2h2V8h2v8h-2v-4h-2Zm-4 0h4v2h-4v-2Zm0 0V8h-2v4h2Zm-8 6H2v2H0v2h2v2h2v-2h2v-2H4v-2Z" /> </svg>} />
<Button2 title="/gamerule" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 5h16v2H4V5zm0 12H2V7h2v10zm16 0v2H4v-2h16zm0 0h2V7h-2v10zm-2-8h-4v6h4V9z" fill="currentColor" /> </svg>} />
<Button2 title="/vanish" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M8 6h8v2H8V6zm-4 4V8h4v2H4zm-2 2v-2h2v2H2zm0 2v-2H0v2h2zm2 2H2v-2h2v2zm4 2H4v-2h4v2zm8 0v2H8v-2h8zm4-2v2h-4v-2h4zm2-2v2h-2v-2h2zm0-2h2v2h-2v-2zm-2-2h2v2h-2v-2zm0 0V8h-4v2h4zm-10 1h4v4h-4v-4z" fill="currentColor" /> </svg>} />
<Button2 title="/clear" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16 2v4h6v2h-2v14H4V8H2V6h6V2h8zm-2 2h-4v2h4V4zm0 4H6v12h12V8h-4z" fill="currentColor" /> </svg>} />
<Button2 title="/setspawnpoint" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M13 2v4h5v5h4v2h-4v5h-5v4h-2v-4H6v-5H2v-2h4V6h5V2h2zM8 8v8h8V8H8zm2 2h4v4h-4v-4z" fill="currentColor" /> </svg>} />
</div>
}
const meta: Meta<any> = {
component: Comp,
}
export default meta
type Story = StoryObj<any>;
export const Primary: Story = {
args: {
},
}

View file

@ -0,0 +1,4 @@
// names: https://pixelarticons.com/free/
export default ({ iconName, width, styles = {}, className = undefined }) => {
return <iconify-icon icon={`pixelarticons:${iconName}`} style={{ width, height: width, ...styles }} className={className} />
}

View file

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import TouchAreasControls from './TouchAreasControls'
const meta: Meta<typeof TouchAreasControls> = {
component: TouchAreasControls,
args: {
},
}
export default meta
type Story = StoryObj<typeof TouchAreasControls>;
export const Primary: Story = {
args: {
touchActive: true,
setupActive: true,
},
}

View file

@ -0,0 +1,109 @@
import { CSSProperties, useEffect, useRef, useState } from 'react'
import PixelartIcon from './PixelartIcon'
export type Button = 'action' | 'sneak' | 'break'
interface Props {
touchActive: boolean
setupActive: boolean
buttonsPositions: Record<Button, [number, number]>
}
export default ({ touchActive, setupActive, buttonsPositions }: Props) => {
if (setupActive) touchActive = true
const [joystickPosition, setJoystickPosition] = useState(null as { x, y, pointerId } | null)
useEffect(() => {
if (!touchActive) return
const controller = new AbortController()
const { signal } = controller
addEventListener('pointerdown', (e) => {
if (e.pointerId === joystickPosition?.pointerId) {
const x = e.clientX - joystickPosition.x
const y = e.clientY - joystickPosition.y
const supportsPressure = (e as any).pressure !== undefined && (e as any).pressure !== 0 && (e as any).pressure !== 0.5 && (e as any).pressure !== 1 && (e.pointerType === 'touch' || e.pointerType === 'pen')
if ((e as any).pressure > 0.5) {
// todo
}
return
}
if (e.clientX < window.innerWidth / 2) {
setJoystickPosition({
x: e.clientX,
y: e.clientY,
pointerId: e.pointerId,
})
}
}, {
signal,
})
return () => {
controller.abort()
}
}, [touchActive])
buttonsPositions = {
// 0-100
action: [
90,
70
],
sneak: [
90,
90
],
break: [
70,
70
]
}
const buttonStyles = (name: Button) => ({
padding: 10,
position: 'fixed',
left: `${buttonsPositions[name][0]}%`,
top: `${buttonsPositions[name][1]}%`,
borderRadius: '50%',
} satisfies CSSProperties)
return <div>
<div
className='movement_joystick_outer'
style={{
display: joystickPosition ? 'block' : 'none',
borderRadius: '50%',
width: 50,
height: 50,
border: '2px solid rgba(0, 0, 0, 0.5)',
backgroundColor: 'rgba(255, 255, div, 0.5)',
position: 'fixed',
left: joystickPosition?.x,
top: joystickPosition?.y,
}}>
<div
className='movement_joystick_inner'
style={{
borderRadius: '50%',
width: 20,
height: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
position: 'absolute',
}}
/>
</div>
<div style={buttonStyles('action')}>
<PixelartIcon width={10} iconName='circle' />
</div>
<div style={buttonStyles('sneak')}>
<PixelartIcon width={10} iconName='arrow-down' />
</div>
<div style={buttonStyles('break')}>
<PixelartIcon width={10} iconName='arrow-down' />
</div>
</div>
}

View file

@ -0,0 +1,13 @@
import { useSnapshot } from 'valtio'
import { activeModalStack } from '../globalState'
import { options } from '../optionsStorage'
import TouchAreasControls from './TouchAreasControls'
import { useIsModalActive, useUsingTouch } from './utils'
export default () => {
const usingTouch = useUsingTouch()
const hasModals = useSnapshot(activeModalStack).length !== 0
const setupActive = useIsModalActive('touch-areas-setup')
return <TouchAreasControls touchActive={!!(usingTouch && hasModals)} setupActive={setupActive} buttonsPositions={options.touchControlsPositions} />
}

View file

@ -14,7 +14,8 @@
flex: 1;
margin: 5px;
overflow: auto;
scrollbar-width: thin;
/* todo think of better workaround */
scrollbar-width: var(--thin-if-firefox);
}
.world_root {

View file

@ -19,6 +19,7 @@ import TouchControls from './react/TouchControls'
import widgets from './react/widgets'
import { useIsWidgetActive } from './react/utils'
import GlobalSearchInput from './GlobalSearchInput'
import TouchAreasControlsProvider from './react/TouchAreasControlsProvider'
const Portal = ({ children, to }) => {
return createPortal(children, to)
@ -59,6 +60,7 @@ const InGameUi = () => {
<DeathScreenProvider />
<ChatProvider />
<SoundMuffler />
<TouchAreasControlsProvider />
</Portal>
<DisplayQr />
<Portal to={document.body}>

View file

@ -187,3 +187,11 @@ export function assertDefined<T> (x: T | undefined): asserts x is T {
export const haveDirectoryPicker = () => {
return !!window.showDirectoryPicker
}
const reportedWarnings = new Set<string>()
export const reportWarningOnce = (id: string, message: string) => {
if (reportedWarnings.has(id)) return
reportedWarnings.add(id)
console.warn(message)
}