Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dist/
*.egg-info/
web/node_modules/
web/dist/
web/test-results/
tmp-e2e/
.DS_Store
.env
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ SNES Studio is a kid-friendly, human-in-the-loop, agent-assisted game builder fo
## Screenshots

### Main Editor UI
![Main Editor UI](web-ui-screenshot.png)
![Main Editor UI](docs/screenshots/main-editor.png)

### Scene Flow Menu
![Scene Flow Menu](docs/screenshots/scene-flow-menu.png)

### Scene Flow Transition
![Scene Flow Transition](docs/screenshots/scene-flow-transition.png)

### Sample Game: Pocket Bugs
![Pocket Bugs Contact Sheet](build/pocket-bugs-listed-contact.png)
Expand Down Expand Up @@ -59,6 +65,7 @@ The agent never silently edits the game. Every helper change is reviewable.
- Discord Activity mode for publishing inside Discord
- Local Python/FastAPI backend mode
- Scene list and scene canvas
- GB Studio-style Scene Flow panel for one-click scene chaining and transitions
- Top-down adventure scenes and platformer / side-scroller scenes
- Tile-based background editor (Zelda/Pokemon-style) with a bundled OpenGameArt overworld tileset, painted to real SNES BG tilemaps
- Actor editing: add, update, delete, move
Expand Down Expand Up @@ -151,6 +158,22 @@ snes-studio add-event-chain examples/hello-human/project.snesproj --id mentor_in
snes-studio add-step examples/hello-human/project.snesproj --chain mentor_intro --type show_text --text "Welcome to SNES Studio."
```

## GB Studio-style scene chaining

The editor includes a **Scene Flow** panel beside the canvas. It is designed for
GB Studio users who expect scene transitions to be managed from a visual scene
menu instead of manually wiring every event chain.

- Pick a scene in the left scene list or in **Scene Flow**.
- Choose a target scene in **Connect current scene to**.
- Click **Add scene transition**.
- SNES Studio creates a trigger zone at the right edge of the current scene.
- It creates a `change_scene` event chain and links that trigger to the target.
- The scene list and Scene Flow panel show outgoing arrows so the project map is readable.

You can still edit the generated trigger bounds, event chain, and dialogue/event
steps manually afterward.

## Building a real ROM: PVSnesLib toolchain setup

The asset/tilemap pipeline and the generated C are produced and tested **without**
Expand Down
9 changes: 9 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SNES Studio opens on **Pocket Bugs**, an original garden-bug battler. You catch
- Browser online demo mode with fallback sample project.
- Local backend mode through FastAPI.
- Scene model with actors, collision rectangles, and trigger zones.
- Scene Flow panel for GB Studio-style scene chaining: choose a target scene and create the trigger + `change_scene` event chain in one action.
- Sprite model with palettes and pixel frames.
- Event-chain model with nested `if_flag` support.
- Block palette definitions for dialogue, actor, scene, logic, timing, and sound actions.
Expand All @@ -24,6 +25,14 @@ SNES Studio opens on **Pocket Bugs**, an original garden-bug battler. You catch
- Integrated EmulatorJS ROM Preview (load and play a homebrew `.sfc`/`.smc` in-browser).
- One-click deploy configs for Vercel (`vercel.json`) and Netlify (`netlify.toml`).

## Current editor screenshots

![Main editor](screenshots/main-editor.png)

![Scene Flow menu](screenshots/scene-flow-menu.png)

![Scene Flow transition](screenshots/scene-flow-transition.png)

## Not included yet

- True Blockly drag/drop workspace.
Expand Down
8 changes: 4 additions & 4 deletions docs/WORKFLOW_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ immediately oriented. The left→centre→right flow is correct.
- **Consistent "import from image" pattern.** Sprites and scenes use the *same*
flow (image/AI art → quantize → preview → apply), with links to GameTorch /
Pixie / Layer.ai / AI Pixel Kit. Learn it once, use it everywhere.
- **One-click scene jumps.** Selecting a trigger or actor and picking "go to
scene" builds and links the `change_scene` chain for you; the Scenes panel
shows outgoing jumps as flow chips. This removes the most tedious part of
hand-building event chains.
- **One-click scene jumps.** The Scene Flow panel lets GB Studio users pick a
target scene and create the trigger + `change_scene` event chain in one action.
Existing trigger/actor "go to scene" shortcuts still work, and the Scenes panel
shows outgoing jumps as flow chips.
- **Templates** give a professional starting point instead of a blank canvas.

## Friction points & recommendations
Expand Down
Binary file added docs/screenshots/main-editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/scene-flow-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/scene-flow-transition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packaging/icons/snes-studio.ico
Binary file not shown.
8 changes: 5 additions & 3 deletions packaging/windows/snes-studio.iss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ OutputBaseFilename=SNES-Studio-Setup
Compression=lzma
SolidCompression=yes
WizardStyle=modern
SetupIconFile=..\icons\snes-studio.ico
UninstallDisplayIcon={app}\{#MyAppExeName}

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Expand All @@ -28,9 +30,9 @@ Name: "modifypath"; Description: "Add snes-studio to PATH"; GroupDescription: "A
Source: "..\..\build\windows\payload\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs

[Icons]
Name: "{autoprograms}\SNES Studio"; Filename: "{app}\{#MyAppExeName}"
Name: "{autoprograms}\SNES Studio CLI"; Filename: "{app}\{#MyCliExeName}"
Name: "{autodesktop}\SNES Studio"; Filename: "{app}\{#MyAppExeName}"
Name: "{autoprograms}\SNES Studio"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{autoprograms}\SNES Studio CLI"; Filename: "{app}\{#MyCliExeName}"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\SNES Studio"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "Launch SNES Studio"; Flags: nowait postinstall skipifsilent
Expand Down
20 changes: 20 additions & 0 deletions scripts/package_macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,33 @@ APP_PAYLOAD="$ROOT/build/macos/root/Applications"
DIST="$ROOT/dist"
mkdir -p "$BIN_PAYLOAD" "$APP_PAYLOAD" "$DIST"

ICON_ARGS=()
ICON_PNG="$ROOT/web/public/branding/snes-studio-icon-1024.png"
ICON_ICNS="$ROOT/build/macos/snes-studio.icns"
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1 && [[ -f "$ICON_PNG" ]]; then
ICONSET="$ROOT/build/macos/snes-studio.iconset"
rm -rf "$ICONSET"
mkdir -p "$ICONSET"
for size in 16 32 64 128 256 512; do
sips -z "$size" "$size" "$ICON_PNG" --out "$ICONSET/icon_${size}x${size}.png" >/dev/null
done
sips -z 32 32 "$ICON_PNG" --out "$ICONSET/icon_16x16@2x.png" >/dev/null
sips -z 64 64 "$ICON_PNG" --out "$ICONSET/icon_32x32@2x.png" >/dev/null
sips -z 256 256 "$ICON_PNG" --out "$ICONSET/icon_128x128@2x.png" >/dev/null
sips -z 512 512 "$ICON_PNG" --out "$ICONSET/icon_256x256@2x.png" >/dev/null
sips -z 1024 1024 "$ICON_PNG" --out "$ICONSET/icon_512x512@2x.png" >/dev/null
iconutil -c icns "$ICONSET" -o "$ICON_ICNS"
ICON_ARGS=(--icon "$ICON_ICNS")
fi

python3 -m PyInstaller --onefile --name snes-studio --distpath "$BIN_PAYLOAD" \
--add-data "snesstudio/assets:snesstudio/assets" \
--add-data "snesstudio/templates:snesstudio/templates" \
scripts/snes_studio_cli.py
chmod +x "$BIN_PAYLOAD/snes-studio"

python3 -m PyInstaller --windowed --name "SNES Studio" --distpath "$APP_PAYLOAD" \
"${ICON_ARGS[@]}" \
--add-data "web/dist:web/dist" \
--add-data "snesstudio/assets:snesstudio/assets" \
--add-data "snesstudio/templates:snesstudio/templates" \
Expand Down
2 changes: 2 additions & 0 deletions scripts/package_windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ New-Item -ItemType Directory -Force -Path $payload | Out-Null
New-Item -ItemType Directory -Force -Path $dist | Out-Null

python -m PyInstaller --onefile --name snes-studio --distpath $payload `
--icon "packaging/icons/snes-studio.ico" `
--add-data "snesstudio/assets;snesstudio/assets" `
--add-data "snesstudio/templates;snesstudio/templates" `
scripts/snes_studio_cli.py
python -m PyInstaller --onefile --windowed --name "SNES Studio" --distpath $payload `
--icon "packaging/icons/snes-studio.ico" `
--add-data "web/dist;web/dist" `
--add-data "snesstudio/assets;snesstudio/assets" `
--add-data "snesstudio/templates;snesstudio/templates" `
Expand Down
8 changes: 7 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>SNES Studio</title></head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SNES Studio</title>
<link rel="icon" href="%BASE_URL%branding/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="%BASE_URL%branding/snes-studio-icon-256.png" />
</head>
<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
</html>
Binary file modified web/public/branding/discord-icon-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/favicon.ico
Binary file not shown.
Binary file added web/public/branding/snes-studio-icon-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web/public/branding/snes-studio-icon-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 48 additions & 2 deletions web/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,35 @@ function SceneHierarchy({ scene, selectedActor, setSelectedActor, selectedZone,
</section>;
}

function SceneFlowPanel({ project, scene, setSceneId, onCreateTransition }) {
const [target, setTarget] = useState('');
const scenes = project?.scenes || [];
const outgoing = scene ? sceneJumpTargets(project, scene.id) : [];
useEffect(() => {
if (!scene) return;
setTarget(t => (t && t !== scene.id && scenes.some(s => s.id === t)) ? t : (scenes.find(s => s.id !== scene.id)?.id || ''));
}, [scene?.id, scenes.length]);
return <section className="card scene-flow-card">
<div className="section-title"><div><h2><GitBranch size={18}/> Scene Flow</h2><p>GB Studio-style scene chaining menu.</p></div><Pill tone="blue">{outgoing.length} exits</Pill></div>
<div className="flow-list">
{scenes.map(s => {
const links = sceneJumpTargets(project, s.id);
return <button key={s.id} className={`flow-scene ${s.id===scene?.id?'active':''}`} onClick={()=>setSceneId(s.id)}>
<span>{s.name}</span>
<small>{s.mode==='platformer'?'Platformer':'Top-down'} · {links.length ? `→ ${links.map(id => scenes.find(x=>x.id===id)?.name || id).join(', ')}` : 'no exits'}</small>
</button>;
})}
</div>
{scene && scenes.length > 1 ? <div className="flow-create">
<label>Connect current scene to<select value={target} onChange={e=>setTarget(e.target.value)}>
{scenes.filter(s=>s.id!==scene.id).map(s=><option key={s.id} value={s.id}>{s.name}</option>)}
</select></label>
<Btn className="secondary full" disabled={!target} onClick={()=>onCreateTransition(target)}><GitBranch size={16}/>Add scene transition</Btn>
<p className="hint">Creates a trigger at the right edge, adds a `change_scene` event chain, and links them automatically.</p>
</div> : <p className="hint">Add another scene to create transitions.</p>}
</section>;
}

function Inspector({ scene, scenes, actor, chains, onUpdate, onDelete, onLinkActorScene, onUpdateScene }) {
const [form, setForm] = useState({});
useEffect(() => { setForm({ name: actor?.name ?? '', x: actor?.x ?? 0, y: actor?.y ?? 0, interact: actor?.events?.interact ?? '' }); }, [actor?.id]);
Expand Down Expand Up @@ -661,6 +690,7 @@ function App(){
const releaseBase = `https://github.com/${releaseRepo}/releases/latest/download`;
const winInstaller = `${releaseBase}/SNES-Studio-Setup.exe`;
const macInstaller = `${releaseBase}/SNES-Studio-macOS.pkg`;
const appIcon = `${import.meta.env.BASE_URL}branding/snes-studio-icon-64.png`;

function applySnap(s){ setMode(s.mode); setProject(s.project); setInventory(s.inventory); setBlocks(s.blocks); client.current.toolchain().then(setToolStatus).catch(()=>{}); }
useEffect(()=>{ client.current=new StudioClient(); client.current.boot().then(s=>{ applySnap(s); setSceneId(s.project?.scenes?.[0]?.id); setChainId(s.project?.eventChains?.[0]?.id); setSpriteId(s.project?.sprites?.[0]?.id); setLog(s.mode==='backend'?`Backend connected: ${client.current.backendTarget()}`:'Online demo mode.'); }).catch(e=>setLog(e.message)); },[]);
Expand Down Expand Up @@ -724,6 +754,22 @@ function App(){
setChainId(chainId);
} catch(e){ setLog(`Could not link scene jump: ${e.message}`); }
}
async function createSceneTransition(targetSceneId){
if(!scene || !targetSceneId) return;
const target = project.scenes.find(s=>s.id===targetSceneId);
const triggerId = uniqueId(`to_${slug(target?.name||targetSceneId)}`, (scene.triggers||[]).map(t=>t.id));
const chainId = uniqueId(`goto_${slug(targetSceneId)}`, (project.eventChains||[]).map(c=>c.id));
const trigger = { id: triggerId, name: `To ${target?.name||targetSceneId}`, x: SCENE_W - 24, y: Math.round(SCENE_H / 2) - 24, w: 24, h: 48 };
try {
await client.current.addTrigger(scene.id, trigger);
await client.current.addChain(chainId, `Go to ${target?.name||targetSceneId}`, { type:'zone_enter', zone: triggerId });
await client.current.addStep(chainId, { id:`${chainId}_go`, type:'change_scene', scene: targetSceneId });
await commit(client.current.updateTrigger(scene.id, triggerId, { event: chainId }), `Scene flow linked ${scene.name} → ${target?.name||targetSceneId}.`);
setSelectedZone({ kind:'trigger', id: triggerId });
setChainId(chainId);
setView('scene');
} catch(e){ setLog(`Could not create scene transition: ${e.message}`); }
}
const onAddStep = (chain, block)=>{ if(!chain) return; const { then, else:_e, ...rest } = block.defaults||{}; const step={ id: uniqueId(slug(block.type), (chain.steps||[]).map(s=>s.id)), type: block.type, ...rest }; if(block.type==='if_flag'){ step.then=[]; step.else=[]; } commit(client.current.addStep(chain.id, step), `Added ${block.label} step.`); };
const onDeleteStep = (cid, sid)=> commit(client.current.deleteStep(cid, sid), 'Removed step.');

Expand Down Expand Up @@ -817,7 +863,7 @@ function App(){
];
const sceneModes = [...new Set((project?.scenes || []).map(s => s.mode || 'topdown'))].map(sceneModeLabel).join(' + ') || 'Top-down adventure';

return <div className="page"><div className="shell"><header className="topbar"><div className="brand"><div className="logo"><Gamepad2/></div><div><h1>SNES Studio</h1><p>Scene editor · sprite painter · event chains · ROM export workflow</p></div></div><div className="actions"><Btn className="secondary" onClick={()=>setGallery(true)}><Blocks size={16}/>Templates</Btn><Btn className="secondary" onClick={openProject}><FolderOpen size={16}/>Open</Btn><Btn className="secondary" onClick={()=>client.current.downloadProject()}><Save size={16}/>Save</Btn><Btn className="secondary" onClick={exportC}><Code2 size={16}/>Export</Btn><Btn className="secondary play-action" onClick={pickRom}><MonitorPlay size={16}/>Play</Btn><Btn className="primary build-action" onClick={build}><Play size={16}/>Build</Btn><button className="icon settings-action" title="Studio settings" onClick={()=>setAiSettings(true)}><Settings2 size={16}/></button></div></header>
return <div className="page"><div className="shell"><header className="topbar"><div className="brand"><div className="logo app-logo"><img src={appIcon} alt="" /><Gamepad2/></div><div><h1>SNES Studio</h1><p>Scene editor · sprite painter · event chains · ROM export workflow</p></div></div><div className="actions"><Btn className="secondary" onClick={()=>setGallery(true)}><Blocks size={16}/>Templates</Btn><Btn className="secondary" onClick={openProject}><FolderOpen size={16}/>Open</Btn><Btn className="secondary" onClick={()=>client.current.downloadProject()}><Save size={16}/>Save</Btn><Btn className="secondary" onClick={exportC}><Code2 size={16}/>Export</Btn><Btn className="secondary play-action" onClick={pickRom}><MonitorPlay size={16}/>Play</Btn><Btn className="primary build-action" onClick={build}><Play size={16}/>Build</Btn><button className="icon settings-action" title="Studio settings" onClick={()=>setAiSettings(true)}><Settings2 size={16}/></button></div></header>
<input ref={fileInput} type="file" accept=".snesproj,.json,application/json" style={{display:'none'}} onChange={onFile}/>
<input ref={romInput} type="file" accept=".sfc,.smc" style={{display:'none'}} onChange={onRomFile}/>
<div className="modebar">{discord.active ? <Gamepad2 size={16}/> : mode==='backend'?<Wifi size={16}/>:<WifiOff size={16}/>}<strong>{discord.active ? 'Discord Activity' : mode==='backend'?'Backend mode':'Online demo mode'}</strong><span>{discord.active ? (discord.ready ? 'Discord SDK ready. Build and share SNES projects in a Discord Activity.' : `Discord SDK not ready: ${discord.error || 'waiting'}`) : log}</span><Btn className="secondary compact" onClick={()=>client.current.downloadProject()}><Download size={16}/>Download project</Btn></div>
Expand All @@ -839,7 +885,7 @@ function App(){
</main>
<aside className="right">
{view==='scene'
? <><SceneHierarchy scene={scene} selectedActor={actor?.id} setSelectedActor={setActorId} selectedZone={selectedZone} setSelectedZone={setSelectedZone}/>{selectedZone ? <SceneTools scene={scene} scenes={project?.scenes||[]} selectedZone={selectedZone} setSelectedZone={setSelectedZone} onUpdateCollision={onUpdateCollision} onDeleteCollision={onDeleteCollision} onUpdateTrigger={onUpdateTrigger} onDeleteTrigger={onDeleteTrigger} onLinkTriggerScene={linkTriggerToScene}/> : <Inspector scene={scene} scenes={project?.scenes||[]} actor={actor} chains={project?.eventChains||[]} onUpdate={onUpdateActor} onDelete={onDeleteActor} onLinkActorScene={linkActorToScene} onUpdateScene={onUpdateScene}/>}</>
? <><SceneHierarchy scene={scene} selectedActor={actor?.id} setSelectedActor={setActorId} selectedZone={selectedZone} setSelectedZone={setSelectedZone}/><SceneFlowPanel project={project} scene={scene} setSceneId={setSceneId} onCreateTransition={createSceneTransition}/>{selectedZone ? <SceneTools scene={scene} scenes={project?.scenes||[]} selectedZone={selectedZone} setSelectedZone={setSelectedZone} onUpdateCollision={onUpdateCollision} onDeleteCollision={onDeleteCollision} onUpdateTrigger={onUpdateTrigger} onDeleteTrigger={onDeleteTrigger} onLinkTriggerScene={linkTriggerToScene}/> : <Inspector scene={scene} scenes={project?.scenes||[]} actor={actor} chains={project?.eventChains||[]} onUpdate={onUpdateActor} onDelete={onDeleteActor} onLinkActorScene={linkActorToScene} onUpdateScene={onUpdateScene}/>}</>
: <Inspector scene={scene} scenes={project?.scenes||[]} actor={actor} chains={project?.eventChains||[]} onUpdate={onUpdateActor} onDelete={onDeleteActor} onLinkActorScene={linkActorToScene} onUpdateScene={onUpdateScene}/>}
<section className="card"><div className="section-title"><h2><Download size={18}/> Installers</h2></div><p className="hint">Download desktop installers from the latest release.</p><div className="two"><a className="btn secondary" href={winInstaller}><Download size={16}/>Windows</a><a className="btn secondary" href={macInstaller}><Download size={16}/>macOS</a></div></section>
{showAiTools ? <section className="card optional-ai"><div className="section-title"><h2><Wand2 size={18}/> Optional Coding Helper</h2><div className="two"><Pill tone={aiOn?'good':'blue'}>{aiOn?'AI on':'offline'}</Pill><button className="icon" title="Studio settings" onClick={()=>setAiSettings(true)}><Settings2 size={16}/></button></div></div><textarea value={prompt} onChange={e=>setPrompt(e.target.value)} placeholder="Optional helper: describe a small reviewed patch, e.g. add dialogue or move an actor."/><Btn className="secondary full" onClick={propose}><Wand2 size={16}/>Draft reviewed patch</Btn>{!aiOn?<p className="hint">Add an Anthropic API key in settings for real AI help. Without a key this uses the safe offline helper.</p>:null}</section> : null}
Expand Down
Loading
Loading