Skip to content

App directory to define apps / edit scripts in IDE #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ COPY --from=builder /app/build ./build
COPY --from=builder /app/src ./src
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/apps ./apps


# Set build argument and environment variable
ARG COMMIT_HASH=local
Expand Down
22 changes: 22 additions & 0 deletions apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Local Apps Development

This directory contains locally developed apps that can be loaded into the game engine without using the in-browser editor.

## Creating a New App

1. Create a new directory in `/apps` with your app name (e.g., `my-app`)
2. Add a `manifest.json` file with the following structure:

```json
{
"name": "My App",
"description": "Description of your app",
"model": "model.glb", // optional - path to 3D model file
"script": "script.js" // optional - path to script file
}
```

3. Add your app files:
- `script.js` - Your app's JavaScript code
- `model.glb` - Your app's 3D model (optional)
- Any other assets your app needs
6 changes: 6 additions & 0 deletions apps/crash-block/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Crash Block",
"description": "A simple block that is loaded locally.",
"model": "model.glb",
"script": "script.js"
}
Binary file added apps/crash-block/model.glb
Binary file not shown.
5 changes: 5 additions & 0 deletions apps/crash-block/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
console.log('Crash Block app loaded!')

app.on('update', delta => {
app.rotation.y += delta * 10
})
47 changes: 39 additions & 8 deletions src/client/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,15 @@ export function Sidebar({ world, ui }) {
>
<SquareMenuIcon size='1.25rem' />
</Btn>
<Btn
active={activePane === 'script'}
suspended={ui.pane === 'script' && !activePane}
onClick={() => world.ui.togglePane('script')}
>
<CodeIcon size='1.25rem' />
</Btn>
{!ui.app.blueprint.isLocal && (
<Btn
active={activePane === 'script'}
suspended={ui.pane === 'script' && !activePane}
onClick={() => world.ui.togglePane('script')}
>
<CodeIcon size='1.25rem' />
</Btn>
)}
<Btn
active={activePane === 'nodes'}
suspended={ui.pane === 'nodes' && !activePane}
Expand All @@ -225,7 +227,7 @@ export function Sidebar({ world, ui }) {
{ui.pane === 'apps' && <Apps world={world} hidden={!ui.active} />}
{ui.pane === 'add' && <Add world={world} hidden={!ui.active} />}
{ui.pane === 'app' && <App key={ui.app.data.id} world={world} hidden={!ui.active} />}
{ui.pane === 'script' && <Script key={ui.app.data.id} world={world} hidden={!ui.active} />}
{ui.pane === 'script' && !ui.app.blueprint.isLocal && <Script key={ui.app.data.id} world={world} hidden={!ui.active} />}
{ui.pane === 'nodes' && <Nodes key={ui.app.data.id} world={world} hidden={!ui.active} />}
{ui.pane === 'meta' && <Meta key={ui.app.data.id} world={world} hidden={!ui.active} />}
</div>
Expand Down Expand Up @@ -849,8 +851,15 @@ function Apps({ world, hidden }) {
function Add({ world, hidden }) {
// note: multiple collections are supported by the engine but for now we just use the 'default' collection.
const collection = world.collections.get('default')
const [localApps, setLocalApps] = useState([])
const span = 4
const gap = '0.5rem'
useEffect(() => {
fetch('/api/apps')
.then(res => res.json())
.then(apps => setLocalApps(apps))
.catch(err => console.error('Failed to load local apps:', err))
}, [])
const add = blueprint => {
blueprint = cloneDeep(blueprint)
blueprint.id = uuid()
Expand Down Expand Up @@ -935,6 +944,22 @@ function Add({ world, hidden }) {
</div>
<div className='add-content noscrollbar'>
<div className='add-items'>
{/* Local Apps */}
{localApps.map(app => (
<div className='add-item' key={app.id} onClick={() => add(app)}>
<div
className='add-item-image'
css={css`
background-image: url(${app.image || '/assets/default-app.png'});
${!app.image && 'display: flex; align-items: center; justify-content: center;'}
`}
>
{!app.image && '📦'}
</div>
<div className='add-item-name'>{app.name} 🛠️</div>
</div>
))}
{/* Collection Apps */}
{collection.blueprints.map(blueprint => (
<div className='add-item' key={blueprint.id} onClick={() => add(blueprint)}>
<div
Expand Down Expand Up @@ -1167,6 +1192,12 @@ function App({ world, hidden }) {
</div>
</div>
<div className='app-content noscrollbar'>
{blueprint.isLocal && (
<div style={{ padding: '1rem', color: 'rgba(255,255,255,0.6)', fontSize: '0.875rem' }}>
This is a local app. Edit the script at: <br />
<code style={{ color: '#4088ff', fontSize: '0.8rem' }}>apps/{blueprint.id}/script.js</code>
</div>
)}
{transforms && <AppTransformFields app={app} />}
<AppFields world={world} app={app} blueprint={blueprint} />
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/core/createServerWorld.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ServerNetwork } from './systems/ServerNetwork'
import { ServerLoader } from './systems/ServerLoader'
import { ServerEnvironment } from './systems/ServerEnvironment'
import { ServerMonitor } from './systems/ServerMonitor'
import { LocalApps } from './systems/LocalApps'


export function createServerWorld() {
const world = new World()
Expand All @@ -15,5 +17,6 @@ export function createServerWorld() {
world.register('loader', ServerLoader)
world.register('environment', ServerEnvironment)
world.register('monitor', ServerMonitor)
world.register('localApps', LocalApps)
return world
}
7 changes: 6 additions & 1 deletion src/core/entities/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ export class App extends Entity {
if (blueprint.script) {
try {
script = this.world.loader.get('script', blueprint.script)
if (!script) script = await this.world.loader.load('script', blueprint.script)
if (!script) {
console.log(`[App] Loading script: ${blueprint.script}`)
script = await this.world.loader.load('script', blueprint.script)
} else {
console.log(`[App] Using cached script: ${blueprint.script}`)
}
} catch (err) {
console.error(err)
crashed = true
Expand Down
1 change: 1 addition & 0 deletions src/core/packets.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const names = [
'kick',
'ping',
'pong',
'clearScriptCache',
]

const byName = {}
Expand Down
6 changes: 6 additions & 0 deletions src/core/systems/Blueprints.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@ export class Blueprints extends System {
}
const changed = !isEqual(blueprint, modified)
if (!changed) return
console.log(`[Blueprints] Modifying blueprint ${blueprint.id}, changed:`, changed)
this.items.set(blueprint.id, modified)
let rebuiltCount = 0
for (const [_, entity] of this.world.entities.items) {
if (entity.data.blueprint === blueprint.id) {
console.log(`[Blueprints] Found entity ${entity.data.id} with matching blueprint`)
entity.data.state = {}
entity.build()
rebuiltCount++
}
}
console.log(`[Blueprints] Rebuilt ${rebuiltCount} entities`)

this.emit('modify', modified)
}

Expand Down
1 change: 1 addition & 0 deletions src/core/systems/ClientLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class ClientLoader extends System {
texture.type = result.type
texture.needsUpdate = true
this.results.set(key, texture)
console.log(`[ClientLoader] Cached script with key: ${key}`)
return texture
}
if (type === 'image') {
Expand Down
39 changes: 39 additions & 0 deletions src/core/systems/ClientNetwork.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,48 @@ export class ClientNetwork extends System {
}

onBlueprintModified = change => {
console.log('[ClientNetwork] Blueprint modified:', change)
this.world.blueprints.modify(change)
}

onClearScriptCache = data => {
console.log(`[ClientNetwork] Clearing script cache for blueprint: ${data.blueprintId}`)

// Clear using the scriptPath directly
const scriptKey = `script/${data.scriptPath}`
console.log(`[ClientNetwork] Clearing cache key: ${scriptKey}`)

if (this.world.loader.results) {
this.world.loader.results.delete(scriptKey)
}
if (this.world.loader.promises) {
this.world.loader.promises.delete(scriptKey)
}

// Also clear the file cache
if (this.world.loader.files) {
this.world.loader.files.delete(data.scriptPath)
// Also clear with resolved URL
const resolvedUrl = this.world.resolveURL(data.scriptPath)
if (resolvedUrl !== data.scriptPath) {
this.world.loader.files.delete(resolvedUrl)
const resolvedKey = `script/${resolvedUrl}`
if (this.world.loader.results) {
this.world.loader.results.delete(resolvedKey)
}
if (this.world.loader.promises) {
this.world.loader.promises.delete(resolvedKey)
}
}
}

console.log('[ClientNetwork] Current cache state after clearing:', {
results: this.world.loader.results ? Array.from(this.world.loader.results.keys()).filter(k => k.includes('crash-block')) : [],
promises: this.world.loader.promises ? Array.from(this.world.loader.promises.keys()).filter(k => k.includes('crash-block')) : [],
files: this.world.loader.files ? Array.from(this.world.loader.files.keys()).filter(k => k.includes('crash-block')) : []
})
}

onEntityAdded = data => {
this.world.entities.add(data)
}
Expand Down
Loading