diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 665abb30b..042302a41 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -32,6 +32,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 18c80e8c7..6408c86a1 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -61,6 +61,8 @@ jobs: echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eb765648d..4ba5aaada 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod + env: + CONFIG_JSON_SOURCE: BUNDLED - run: pnpm build-storybook - name: Copy playground files run: | diff --git a/Dockerfile b/Dockerfile index 4769141f7..346413536 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN npm i -g pnpm@9.0.4 # Build arguments ARG DOWNLOAD_SOUNDS=false ARG DISABLE_SERVICE_WORKER=false +ARG CONFIG_JSON_SOURCE=REMOTE # TODO need flat --no-root-optional RUN node ./scripts/dockerPrepare.mjs RUN pnpm i @@ -22,8 +23,8 @@ RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs # ENTRYPOINT ["pnpm", "run", "run-all"] # only for prod -RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \ - DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ +RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \ + CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \ pnpm run build # ---- Run Stage ---- diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 4147ce7d7..875d0e0c7 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -25,12 +25,14 @@ const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true' let releaseTag let releaseLink let releaseChangelog +let githubRepositoryFallback if (fs.existsSync('./assets/release.json')) { const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) releaseTag = releaseJson.latestTag releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}` releaseChangelog = releaseJson.changelog?.replace(//, '') + githubRepositoryFallback = releaseJson.repository } const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8')) @@ -41,6 +43,8 @@ if (dev) { configJson.defaultProxy = ':8080' } +const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE' + // base options are in ./renderer/rsbuildSharedConfig.ts const appConfig = defineConfig({ html: { @@ -66,13 +70,13 @@ const appConfig = defineConfig({ 'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'), 'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS), 'process.env.GITHUB_URL': - JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`), + JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`), 'process.env.DEPS_VERSIONS': JSON.stringify({}), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), - 'process.env.INLINED_APP_CONFIG': JSON.stringify(configJson), + 'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null), }, }, server: { @@ -109,7 +113,9 @@ const appConfig = defineConfig({ fs.copyFileSync('./assets/release.json', './dist/release.json') } - fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') + if (configSource === 'REMOTE') { + fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8') + } if (fs.existsSync('./generated/sounds.js')) { fs.copyFileSync('./generated/sounds.js', './dist/sounds.js') } diff --git a/scripts/dockerPrepare.mjs b/scripts/dockerPrepare.mjs index 37e57d015..62a4f5e4e 100644 --- a/scripts/dockerPrepare.mjs +++ b/scripts/dockerPrepare.mjs @@ -4,9 +4,27 @@ import path from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' -// write release tag +// Get repository from git config +const getGitRepository = () => { + try { + const gitConfig = fs.readFileSync('.git/config', 'utf8') + const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m) + if (originUrlMatch) { + return originUrlMatch[1] + } + } catch (err) { + console.warn('Failed to read git repository from config:', err) + } + return null +} + +// write release tag and repository info const commitShort = execSync('git rev-parse --short HEAD').toString().trim() -fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8') +const repository = getGitRepository() +fs.writeFileSync('./assets/release.json', JSON.stringify({ + latestTag: `${commitShort} (docker)`, + repository +}), 'utf8') const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')) delete packageJson.optionalDependencies diff --git a/src/appConfig.ts b/src/appConfig.ts new file mode 100644 index 000000000..3d6d8f93a --- /dev/null +++ b/src/appConfig.ts @@ -0,0 +1,59 @@ +import { disabledSettings, options, qsOptions } from './optionsStorage' +import { miscUiState } from './globalState' +import { setLoadingScreenStatus } from './appStatus' + +export type AppConfig = { + // defaultHost?: string + // defaultHostSave?: string + defaultProxy?: string + // defaultProxySave?: string + // defaultVersion?: string + peerJsServer?: string + peerJsServerFallback?: string + promoteServers?: Array<{ ip, description, version? }> + mapsProvider?: string + + appParams?: Record // query string params + + defaultSettings?: Record + forceSettings?: Record + // hideSettings?: Record + allowAutoConnect?: boolean + pauseLinks?: Array>> +} + +export const loadAppConfig = (appConfig: AppConfig) => { + if (miscUiState.appConfig) { + Object.assign(miscUiState.appConfig, appConfig) + } else { + miscUiState.appConfig = appConfig + } + + if (appConfig.forceSettings) { + for (const [key, value] of Object.entries(appConfig.forceSettings)) { + if (value) { + disabledSettings.value.add(key) + // since the setting is forced, we need to set it to that value + if (appConfig.defaultSettings?.[key] && !qsOptions[key]) { + options[key] = appConfig.defaultSettings[key] + } + } else { + disabledSettings.value.delete(key) + } + } + } +} + +export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG + +if (isBundledConfigUsed) { + loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) +} else { + void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { + // console.warn('Failed to load optional app config.json', error) + // return {} + setLoadingScreenStatus('Failed to load app config.json', true) + }).then((config: AppConfig) => { + loadAppConfig(config) + }) +} diff --git a/src/appParams.ts b/src/appParams.ts index 50fbf0554..786e05f9c 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from './globalState' +import type { AppConfig } from './appConfig' const qsParams = new URLSearchParams(window.location?.search ?? '') diff --git a/src/browserfs.ts b/src/browserfs.ts index 41608e308..0f4579b8e 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -41,13 +41,13 @@ browserfs.configure({ throw e2 } showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true) - miscUiState.appLoaded = true + miscUiState.fsReady = true miscUiState.singleplayerAvailable = false }) return } await updateTexturePackInstalledState() - miscUiState.appLoaded = true + miscUiState.fsReady = true miscUiState.singleplayerAvailable = true }) diff --git a/src/customChannels.ts b/src/customChannels.ts index 35922101c..ff0f8a32d 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -85,15 +85,15 @@ const registeredJeiChannel = () => { [ { name: 'id', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, { name: 'categoryTitle', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, { name: 'items', - type: 'pstring', + type: ['pstring', { countType: 'i16' }] }, ] ] diff --git a/src/globalState.ts b/src/globalState.ts index 74e6c3ffd..0ee8671d6 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -5,6 +5,7 @@ import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import type { OptionsGroupType } from './optionsGuiScheme' import { appQueryParams } from './appParams' import { options, disabledSettings } from './optionsStorage' +import { AppConfig } from './appConfig' // todo: refactor structure with support of hideNext=false @@ -110,26 +111,6 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY }) // --- -export type AppConfig = { - // defaultHost?: string - // defaultHostSave?: string - defaultProxy?: string - // defaultProxySave?: string - // defaultVersion?: string - peerJsServer?: string - peerJsServerFallback?: string - promoteServers?: Array<{ ip, description, version? }> - mapsProvider?: string - - appParams?: Record // query string params - - defaultSettings?: Record - forceSettings?: Record - // hideSettings?: Record - allowAutoConnect?: boolean - pauseLinks?: Array>> -} - export const miscUiState = proxy({ currentDisplayQr: null as string | null, currentTouch: null as boolean | null, @@ -144,7 +125,7 @@ export const miscUiState = proxy({ loadedServerIndex: '', /** currently trying to load or loaded mc version, after all data is loaded */ loadedDataVersion: null as string | null, - appLoaded: false, + fsReady: false, singleplayerAvailable: false, usingGamepadInput: false, appConfig: null as AppConfig | null, @@ -152,24 +133,6 @@ export const miscUiState = proxy({ displayFullmap: false }) -export const loadAppConfig = (appConfig: AppConfig) => { - if (miscUiState.appConfig) { - Object.assign(miscUiState.appConfig, appConfig) - } else { - miscUiState.appConfig = appConfig - } - - if (appConfig.forceSettings) { - for (const [key, value] of Object.entries(appConfig.forceSettings)) { - if (value) { - disabledSettings.value.delete(key) - } else { - disabledSettings.value.add(key) - } - } - } -} - export const isGameActive = (foregroundCheck: boolean) => { if (foregroundCheck && activeModalStack.length) return false return miscUiState.gameLoaded diff --git a/src/index.ts b/src/index.ts index 8e586a8f3..20b9e2ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import './mineflayer/cameraShake' import './shims/patchShims' import './mineflayer/java-tester/index' import './external' +import './appConfig' import { getServerInfo } from './mineflayer/mc-protocol' import { onGameLoad, renderSlot } from './inventoryWindows' import { GeneralInputItem, RenderItem } from './mineflayer/items' @@ -48,7 +49,6 @@ import initializePacketsReplay from './packetsReplay/packetsReplayLegacy' import { initVR } from './vr' import { - AppConfig, activeModalStack, activeModalStacks, hideModal, @@ -57,7 +57,6 @@ import { miscUiState, showModal, gameAdditionalState, - loadAppConfig } from './globalState' import { parseServerAddress } from './parseServerAddress' @@ -903,8 +902,9 @@ export async function connect (connectOptions: ConnectOptions) { const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined listenGlobalEvents() -watchValue(miscUiState, async s => { - if (s.appLoaded) { // fs ready +const unsubscribe = watchValue(miscUiState, async s => { + if (s.fsReady && s.appConfig) { + unsubscribe() if (reconnectOptions) { sessionStorage.removeItem('reconnectOptions') if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) { @@ -965,15 +965,6 @@ document.body.addEventListener('touchstart', (e) => { }, { passive: false }) // #endregion -loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {}) -// load maybe updated config on the server with updated params (just in case) -void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => { - console.warn('Failed to load optional app config.json', error) - return {} -}).then((config: AppConfig | {}) => { - loadAppConfig(config) -}) - // qs open actions if (!reconnectOptions) { downloadAndOpenFile().then((downloadAction) => { diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index b88b71c4a..0ee21657e 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -5,7 +5,7 @@ import { proxy, subscribe } from 'valtio/vanilla' import { subscribeKey } from 'valtio/utils' import { omitObj } from '@zardoy/utils' import { appQueryParamsArray } from './appParams' -import type { AppConfig } from './globalState' +import type { AppConfig } from './appConfig' const isDev = process.env.NODE_ENV === 'development' const initialAppConfig = process.env.INLINED_APP_CONFIG as AppConfig ?? {} @@ -188,7 +188,7 @@ subscribe(options, () => { localStorage.options = JSON.stringify(saveOptions) }) -type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => void +type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void export const watchValue: WatchValue = (proxy, callback) => { const watchedProps = new Set() @@ -198,10 +198,19 @@ export const watchValue: WatchValue = (proxy, callback) => { return Reflect.get(target, p, receiver) }, }), false) + const unsubscribes = [] as Array<() => void> for (const prop of watchedProps) { - subscribeKey(proxy, prop, () => { - callback(proxy, true) - }) + unsubscribes.push( + subscribeKey(proxy, prop, () => { + callback(proxy, true) + }) + ) + } + + return () => { + for (const unsubscribe of unsubscribes) { + unsubscribe() + } } } diff --git a/src/panorama.ts b/src/panorama.ts index efc06e16e..3c888246d 100644 --- a/src/panorama.ts +++ b/src/panorama.ts @@ -48,7 +48,7 @@ const updateResourcePackSupportPanorama = async () => { } watchValue(miscUiState, m => { - if (m.appLoaded) { + if (m.fsReady) { // Also adds panorama on app load here watchValue(resourcePackState, async (s) => { const oldState = panoramaUsesResourcePack diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index e06ced528..30fd3047a 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -75,12 +75,12 @@ export const mainMenuState = proxy({ let disableAnimation = false export default () => { const haveModals = useSnapshot(activeModalStack).length - const { gameLoaded, appLoaded, appConfig, singleplayerAvailable } = useSnapshot(miscUiState) + const { gameLoaded, fsReady, appConfig, singleplayerAvailable } = useSnapshot(miscUiState) - const noDisplay = haveModals || gameLoaded || !appLoaded + const noDisplay = haveModals || gameLoaded || !fsReady useEffect(() => { - if (noDisplay && appLoaded) disableAnimation = true + if (noDisplay && fsReady) disableAnimation = true }, [noDisplay]) const [versionStatus, setVersionStatus] = useState('')