From e2452d17ff3fd403e5dad723f9c8fbe1762d027e Mon Sep 17 00:00:00 2001 From: Clyde013 <40514241+Clyde013@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:39:44 +0800 Subject: [PATCH 1/3] feat: disable standalone mode on macOS --- electron/ipc/server.ts | 6 ++++++ electron/ipc/settings.ts | 25 +++++++++++++++------- src/components/MenuSettingsView.tsx | 18 ++++++++++++---- src/components/ui/SettingsToggle.tsx | 32 ++++++++++++++++++++-------- src/hooks/useSettings.tsx | 3 ++- 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/electron/ipc/server.ts b/electron/ipc/server.ts index a942ba5f..cc7c3bda 100644 --- a/electron/ipc/server.ts +++ b/electron/ipc/server.ts @@ -21,8 +21,14 @@ function isLocalhost(hostname: string): boolean { return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' } +const IS_MACOS = process.platform === 'darwin' + export function registerServerIpc(): void { ipcMain.handle('start-engine-server', async (_event, port: number) => { + if (IS_MACOS) { + throw new Error('Standalone mode is unavailable on macOS. Use server mode with a remote World Engine.') + } + const engineDir = getEngineDir() const uvDir = getUvDir() const uvBinary = getUvBinaryPath() diff --git a/electron/ipc/settings.ts b/electron/ipc/settings.ts index bfbe1856..ce89ffcb 100644 --- a/electron/ipc/settings.ts +++ b/electron/ipc/settings.ts @@ -7,6 +7,15 @@ import type { Settings } from '../../src/types/settings.js' const SETTINGS_FILENAME = 'settings.json' const LEGACY_CONFIG_FILENAME = 'config.json' +const IS_MACOS = process.platform === 'darwin' + +function normalizeSettingsForPlatform(settings: Settings): Settings { + if (!IS_MACOS) return settings + return { + ...settings, + engine_mode: 'server' + } +} function getSettingsPath(): string { const configDir = getConfigDir() @@ -93,7 +102,7 @@ function readSettingsSync(): Settings { const legacyContent = fs.readFileSync(legacyPath, 'utf-8') const legacyParsed = JSON.parse(legacyContent) as Record const migrated = migrateFromLegacyConfig(legacyParsed) - const result = settingsSchema.parse(migrated) + const result = normalizeSettingsForPlatform(settingsSchema.parse(migrated)) // Write migrated settings fs.writeFileSync(settingsPath, JSON.stringify(result, null, 2)) @@ -105,7 +114,7 @@ function readSettingsSync(): Settings { } // No existing files — use defaults - const defaults = settingsSchema.parse({}) + const defaults = normalizeSettingsForPlatform(settingsSchema.parse({})) fs.writeFileSync(settingsPath, JSON.stringify(defaults, null, 2)) return defaults } @@ -116,18 +125,18 @@ function readSettingsSync(): Settings { parsed = JSON.parse(content) } catch { console.warn('[SETTINGS] Failed to parse settings.json, using defaults') - const defaults = settingsSchema.parse({}) + const defaults = normalizeSettingsForPlatform(settingsSchema.parse({})) fs.writeFileSync(settingsPath, JSON.stringify(defaults, null, 2)) return defaults } const result = settingsSchema.safeParse(parsed) if (result.success) { - return result.data + return normalizeSettingsForPlatform(result.data) } console.warn('[SETTINGS] Invalid settings.json, using defaults:', result.error.message) - const defaults = settingsSchema.parse({}) + const defaults = normalizeSettingsForPlatform(settingsSchema.parse({})) fs.writeFileSync(settingsPath, JSON.stringify(defaults, null, 2)) return defaults } @@ -146,12 +155,12 @@ export function registerSettingsIpc(): void { }) ipcMain.handle('read-default-settings', () => { - return settingsSchema.parse({}) + return normalizeSettingsForPlatform(settingsSchema.parse({})) }) ipcMain.handle('write-settings', (_event, settings: Settings) => { const settingsPath = getSettingsPath() - const validated = settingsSchema.parse(settings) + const validated = normalizeSettingsForPlatform(settingsSchema.parse(settings)) fs.writeFileSync(settingsPath, JSON.stringify(validated, null, 2)) }) @@ -162,7 +171,7 @@ export function registerSettingsIpc(): void { ipcMain.handle('open-settings', () => { const settingsPath = getSettingsPath() if (!fs.existsSync(settingsPath)) { - const defaults = settingsSchema.parse({}) + const defaults = normalizeSettingsForPlatform(settingsSchema.parse({})) fs.writeFileSync(settingsPath, JSON.stringify(defaults, null, 2)) } shell.showItemInFolder(settingsPath) diff --git a/src/components/MenuSettingsView.tsx b/src/components/MenuSettingsView.tsx index b8f036b9..6aa09160 100644 --- a/src/components/MenuSettingsView.tsx +++ b/src/components/MenuSettingsView.tsx @@ -27,6 +27,8 @@ type MenuModelOption = { sizeBytes: number | null } +const IS_MACOS = navigator.platform.toLowerCase().includes('mac') + function formatBytes(bytes: number): string { if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB` return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` @@ -83,7 +85,7 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => { const configWorldModel = settings.engine_model const [menuEngineMode, setMenuEngineMode] = useState<'server' | 'standalone'>(() => - configEngineMode === ENGINE_MODES.SERVER ? 'server' : 'standalone' + IS_MACOS || configEngineMode === ENGINE_MODES.SERVER ? 'server' : 'standalone' ) const [menuWorldModel, setMenuWorldModel] = useState(configWorldModel) const [menuMouseSensitivity, setMenuMouseSensitivity] = useState(() => @@ -214,7 +216,7 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => { }, [menuWorldModel, menuEngineMode, serverUrlForModels, serverUrlStatus]) useEffect(() => { - setMenuEngineMode(configEngineMode === ENGINE_MODES.SERVER ? 'server' : 'standalone') + setMenuEngineMode(IS_MACOS || configEngineMode === ENGINE_MODES.SERVER ? 'server' : 'standalone') setMenuWorldModel(configWorldModel) setMenuMouseSensitivity(streamingToMenu(settings.mouse_sensitivity ?? mouseSensitivity)) setMenuServerUrl(configServerUrl) @@ -268,6 +270,7 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => { }, [menuServerUrl, configServerUrl, lastValidatedServerUrl, serverUrlStatus]) const handleEngineModeChange = (mode: 'server' | 'standalone') => { + if (IS_MACOS && mode === 'standalone') return setMenuEngineMode(mode) setServerUrlStatus('idle') setLastValidatedServerUrl('') @@ -430,7 +433,14 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => { > { )} - {menuEngineMode === 'standalone' && ( + {!IS_MACOS && menuEngineMode === 'standalone' && ( setShowFixModal(true)} diff --git a/src/components/ui/SettingsToggle.tsx b/src/components/ui/SettingsToggle.tsx index 4bb35fc5..6063bcd4 100644 --- a/src/components/ui/SettingsToggle.tsx +++ b/src/components/ui/SettingsToggle.tsx @@ -1,7 +1,14 @@ import SettingsButton from './SettingsButton' +type SettingsToggleOption = { + value: string + label: string + disabled?: boolean + disabledTooltip?: string +} + type SettingsToggleProps = { - options: { value: string; label: string }[] + options: SettingsToggleOption[] value: string onChange: (value: string) => void } @@ -9,14 +16,21 @@ type SettingsToggleProps = { const SettingsToggle = ({ options, value, onChange }: SettingsToggleProps) => (
{options.map((option) => ( - onChange(option.value)} - > - {option.label} - + + onChange(option.value)} + disabled={option.disabled} + aria-disabled={option.disabled ? 'true' : undefined} + > + {option.label} + + ))}
) diff --git a/src/hooks/useSettings.tsx b/src/hooks/useSettings.tsx index 2db28d05..058365bd 100644 --- a/src/hooks/useSettings.tsx +++ b/src/hooks/useSettings.tsx @@ -65,7 +65,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { const saveSettings = useCallback(async (newSettings: Settings) => { try { await invoke('write-settings', newSettings) - setSettings(newSettings) + const persisted = await invoke('read-settings') + setSettings(persisted) setError(null) return true } catch (err) { From 9214bd70c186f70d95264c03e587bb05e9871740 Mon Sep 17 00:00:00 2001 From: Clyde013 <40514241+Clyde013@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:48:13 +0800 Subject: [PATCH 2/3] feat: macOS build --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 2 ++ forge.config.ts | 18 ++++++++++-------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dccbce47..fd0ee244 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: include: - platform: windows-latest - platform: ubuntu-22.04 + - platform: macos-latest runs-on: ${{ matrix.platform }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b866032..e990f31e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: include: - platform: windows-latest - platform: ubuntu-22.04 + - platform: macos-latest runs-on: ${{ matrix.platform }} @@ -64,5 +65,6 @@ jobs: files: | artifacts/**/*.exe artifacts/**/*.AppImage + artifacts/**/*.dmg prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} generate_release_notes: true diff --git a/forge.config.ts b/forge.config.ts index 0de7e08a..5ffdbf3e 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -4,20 +4,22 @@ import MakerNSIS from '@felixrieseberg/electron-forge-maker-nsis' import { MakerDMG } from '@electron-forge/maker-dmg' import { MakerAppImage } from '@reforged/maker-appimage' +const extraResources = [ + ...(process.platform === 'darwin' ? [] : ['./server-components']), + './seeds', + './licensing', + './backgrounds', + './app-icon.ico', + './app-icon.png' +] + const config: ForgeConfig = { packagerConfig: { asar: true, executableName: 'biome', icon: './app-icon', appCopyright: 'Copyright © 2026 Overworld', - extraResource: [ - './server-components', - './seeds', - './licensing', - './backgrounds', - './app-icon.ico', - './app-icon.png' - ] + extraResource: extraResources }, makers: [ new MakerNSIS({ From 04714e042e6fb9dbd6404dfcf7fc652c20bb26cc Mon Sep 17 00:00:00 2001 From: Clyde013 <40514241+Clyde013@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:09:28 +0800 Subject: [PATCH 3/3] feat: macOS signing --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ .prettierignore | 1 + build/entitlements.mac.plist | 12 ++++++++++++ forge.config.ts | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 build/entitlements.mac.plist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e990f31e..4f80d490 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,8 +38,37 @@ jobs: - name: Install dependencies run: npm ci + - name: Import Apple code-signing certificate + if: runner.os == 'macOS' + env: + APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} + APPLE_CERTIFICATE_P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }} + run: | + if [ -z "$APPLE_CERTIFICATE_P12_BASE64" ] || [ -z "$APPLE_CERTIFICATE_P12_PASSWORD" ]; then + echo "Missing Apple certificate secrets. Add APPLE_CERTIFICATE_P12_BASE64 and APPLE_CERTIFICATE_P12_PASSWORD." + exit 1 + fi + + KEYCHAIN_PASSWORD="$(openssl rand -base64 24)" + CERT_PATH="$RUNNER_TEMP/certificate.p12" + + echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH" + + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import "$CERT_PATH" -k build.keychain -P "$APPLE_CERTIFICATE_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain + security find-identity -v -p codesigning build.keychain + - name: Build Electron app run: npm run build + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + MAC_CODESIGN_IDENTITY: ${{ secrets.MAC_CODESIGN_IDENTITY }} - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/.prettierignore b/.prettierignore index a74f682d..2cbee364 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ server-components out .vite build/installer.nsh +build/entitlements.mac.plist diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 00000000..9a279dc8 --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/forge.config.ts b/forge.config.ts index 5ffdbf3e..1124bb0e 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -4,6 +4,23 @@ import MakerNSIS from '@felixrieseberg/electron-forge-maker-nsis' import { MakerDMG } from '@electron-forge/maker-dmg' import { MakerAppImage } from '@reforged/maker-appimage' +const shouldSignMac = + process.platform === 'darwin' && (Boolean(process.env.CSC_LINK) || Boolean(process.env.MAC_CODESIGN_IDENTITY)) + +const shouldNotarizeMac = + shouldSignMac && + Boolean(process.env.APPLE_ID) && + Boolean(process.env.APPLE_APP_SPECIFIC_PASSWORD) && + Boolean(process.env.APPLE_TEAM_ID) + +const macNotarizeCredentials = shouldNotarizeMac + ? { + appleId: process.env.APPLE_ID as string, + appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD as string, + teamId: process.env.APPLE_TEAM_ID as string + } + : undefined + const extraResources = [ ...(process.platform === 'darwin' ? [] : ['./server-components']), './seeds', @@ -17,9 +34,21 @@ const config: ForgeConfig = { packagerConfig: { asar: true, executableName: 'biome', + appBundleId: 'ai.overworld.biome', + appCategoryType: 'public.app-category.games', icon: './app-icon', - appCopyright: 'Copyright © 2026 Overworld', - extraResource: extraResources + appCopyright: 'Copyright (c) 2026 Overworld', + extraResource: extraResources, + osxSign: shouldSignMac + ? { + identity: process.env.MAC_CODESIGN_IDENTITY || undefined, + optionsForFile: () => ({ + hardenedRuntime: true, + entitlements: 'build/entitlements.mac.plist' + }) + } + : undefined, + osxNotarize: macNotarizeCredentials }, makers: [ new MakerNSIS({