Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
include:
- platform: windows-latest
- platform: ubuntu-22.04
- platform: macos-latest

runs-on: ${{ matrix.platform }}

Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
include:
- platform: windows-latest
- platform: ubuntu-22.04
- platform: macos-latest

runs-on: ${{ matrix.platform }}

Expand All @@ -37,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
Expand All @@ -64,5 +94,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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ server-components
out
.vite
build/installer.nsh
build/entitlements.mac.plist
12 changes: 12 additions & 0 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
6 changes: 6 additions & 0 deletions electron/ipc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 17 additions & 8 deletions electron/ipc/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -93,7 +102,7 @@ function readSettingsSync(): Settings {
const legacyContent = fs.readFileSync(legacyPath, 'utf-8')
const legacyParsed = JSON.parse(legacyContent) as Record<string, unknown>
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))
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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))
})

Expand All @@ -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)
Expand Down
49 changes: 40 additions & 9 deletions forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,51 @@ 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',
'./licensing',
'./backgrounds',
'./app-icon.ico',
'./app-icon.png'
]

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: [
'./server-components',
'./seeds',
'./licensing',
'./backgrounds',
'./app-icon.ico',
'./app-icon.png'
]
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({
Expand Down
18 changes: 14 additions & 4 deletions src/components/MenuSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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(() =>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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('')
Expand Down Expand Up @@ -430,7 +433,14 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => {
>
<SettingsToggle
options={[
{ value: 'standalone', label: 'Standalone' },
{
value: 'standalone',
label: 'Standalone',
disabled: IS_MACOS,
disabledTooltip: IS_MACOS
? 'Standalone mode is currently unavailable on macOS. We are working on integrating support, check back in a future version!'
: undefined
},
{ value: 'server', label: 'Server' }
]}
value={menuEngineMode}
Expand Down Expand Up @@ -481,7 +491,7 @@ const MenuSettingsView = ({ onBack }: MenuSettingsViewProps) => {
</SettingsSection>
)}

{menuEngineMode === 'standalone' && (
{!IS_MACOS && menuEngineMode === 'standalone' && (
<WorldEngineSection
engineReady={engineReady}
onFixInPlaceClick={() => setShowFixModal(true)}
Expand Down
32 changes: 23 additions & 9 deletions src/components/ui/SettingsToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
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
}

const SettingsToggle = ({ options, value, onChange }: SettingsToggleProps) => (
<div className="flex">
{options.map((option) => (
<SettingsButton
key={option.value}
variant={value === option.value ? 'primary' : 'secondary'}
className="flex-1"
onClick={() => onChange(option.value)}
>
{option.label}
</SettingsButton>
<span key={option.value} className="flex-1" title={option.disabled ? option.disabledTooltip : undefined}>
<SettingsButton
variant={value === option.value ? 'primary' : 'secondary'}
className={
option.disabled
? 'flex-1 w-full opacity-55 cursor-not-allowed hover:bg-surface-btn-secondary hover:text-text-primary hover:translate-y-0'
: 'flex-1 w-full'
}
onClick={() => onChange(option.value)}
disabled={option.disabled}
aria-disabled={option.disabled ? 'true' : undefined}
>
{option.label}
</SettingsButton>
</span>
))}
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading