Compare commits
2
Commits
62b5f640fc
...
8391ebcd98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8391ebcd98 | ||
|
|
3ab43e84f1 |
+2
-1
@@ -550,7 +550,7 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
|
||||
|
||||
| ID | Decision |
|
||||
|---|---|
|
||||
| D327 | Desktop-first onboarding. Installing, creating and opening projects, testing and exporting need no terminal on Windows, macOS and Linux; the CLI stays for agents and CI. Every desktop entry point (the Windows shortcut and file associations for `.aego`, `.aescene`, `.aeprefab` and folders, the macOS app's document types, the Linux AppImage and the `.desktop` file from install.sh) starts the same `edit.Start` as `aego edit`: without a project it opens the launcher, a file argument opens its project and, for a scene or prefab, that file (`-open`, passed through the supervisor), a native project goes to the supervisor when Go is found and otherwise opens for scripts with a status-bar note. GUI builds write logs to the user cache (`aego/logs`, last ten) and show fatal errors in a message box; child processes are started without console windows (`app/proc`), and the supervised editor is built with `-H=windowsgui`. Native dialogs are `platform.FileDialogs` on the window, implemented with SDL's folder and file dialogs and delivered through a registered SDL event; headless windows have none and the UI falls back to a text field. The launcher creates projects in `Documents/Aego Projects` by default (XDG documents on Linux), offers native code only with Go and engine sources, opens dropped folders and `project.aego`, and has Open, Show in the file manager and Remove on recent projects. Once a day it asks the Gitea API for the latest release and shows a banner linking the download page; it never updates by itself and the check can be turned off (`UserConfigDir/aego/update`). Switching projects from the editor (New, Open, Open Recent, Close Project, and dropping another project's folder) starts the original entry point again (`AEGO_EDITOR_EXE`, `AEGO_EDITOR_VERB`, `$APPIMAGE`) without the supervised flag and quits, after asking about unsaved documents. Play → Run Script Tests runs `aego test` in-process with results in Console; the Export panel takes an output folder and can open or run the result, serving web builds on 127.0.0.1 with COOP/COEP. The home and install pages carry download buttons that gendocs renders at site build time from the latest Gitea release (`site.FetchRelease`, `downloadBlock`): one list item per file that the release has, and `site.js` turns the main button into the visitor's system, or into a link to the install page when the release has no file for it. The app icon (D301) is the window icon of the launcher and the editor through `platform.SetAppIcon`, set in `edit.Start` before the first window. |
|
||||
| D327 | Desktop-first onboarding. Installing, creating and opening projects, testing and exporting need no terminal on Windows, macOS and Linux; the CLI stays for agents and CI. Every desktop entry point (the Windows shortcut and file associations for `.aego`, `.aescene`, `.aeprefab` and folders, the macOS app's document types, the Linux AppImage and the `.desktop` file from install.sh) starts the same `edit.Start` as `aego edit`: without a project it opens the launcher, a file argument opens its project and, for a scene or prefab, that file (`-open`, passed through the supervisor), a native project goes to the supervisor when Go is found and otherwise opens for scripts with a status-bar note. GUI builds write logs to the user cache (`aego/logs`, last ten) and show fatal errors in a message box; child processes are started without console windows (`app/proc`), and the supervised editor is built with `-H=windowsgui`. Native dialogs are `platform.FileDialogs` on the window, implemented with SDL's folder and file dialogs and delivered through a registered SDL event; headless windows have none and the UI falls back to a text field. The launcher creates projects in `Documents/Aego Projects` by default (XDG documents on Linux), offers native code only with Go and engine sources, opens dropped folders and `project.aego`, and has Open, Show in the file manager and Remove on recent projects. Once a day it asks the Gitea API for the latest release and shows a banner linking the download page; it never updates by itself and the check can be turned off (`UserConfigDir/aego/update`). Switching projects from the editor (New, Open, Open Recent, Close Project, and dropping another project's folder) starts the original entry point again (`AEGO_EDITOR_EXE`, `AEGO_EDITOR_VERB`, `$APPIMAGE`) without the supervised flag and quits, after asking about unsaved documents. Play → Run Script Tests runs `aego test` in-process with results in Console; the Export panel takes an output folder and can open or run the result, serving web builds on 127.0.0.1 with COOP/COEP. The home and install pages carry download buttons that gendocs renders at site build time from the latest Gitea release (`site.FetchRelease`, `downloadBlock`): one list item per file that the release has, and `site.js` turns the main button into the visitor's system, or into a link to the install page when the release has no file for it. The app icon (D301) is the window icon of the launcher and the editor through `platform.SetAppIcon`, set in `edit.Start` before the first window. `edit.Start` writes a boot log to `<user cache>/aego/logs/boot.log` (the previous run is kept as `boot.prev.log`): version, executable, arguments, working directory, Go, then each startup step (SDL init, window, GL version, launcher, project, missing Go). A failed OpenGL 3.3 core context probes a default context and names what the driver has (`gl: … driver has GDI Generic 1.1.0`). A start error or panic shows a message box with the log path; on Windows through `MessageBoxW` (`platform.ShowFatal`, usable before and after `SDL_Init`), elsewhere through the SDL alert. |
|
||||
|
||||
## 4. Dependency rules
|
||||
|
||||
@@ -1009,6 +1009,7 @@ Packages are named by capability (`render/view`, `asset/loader`, `scene/sceneio`
|
||||
| 9.5c | Web export, docs session with Inspector and Game View, Try it, playground sandbox |
|
||||
| 9.6 | Agent tooling: `scene/mutate` (transactions with exact inverses and redo, data documents), `project/refs`, `project/refactor`, `project/rename`, editor structure edits through mutate, `app/agent` command catalog and `aego <group> <verb>` CLI, `aego mcp`, external edits in the editor |
|
||||
| 9.7 | Docs and release: editor theme and icons on the site, build-time highlighting with links, short-name references, Try it progress, cache and scene tree, Try scenes for every 2D physics component, script examples on component pages, Script API index and module examples, diagnostics with checked examples, install page, release builds and install script, editor screenshots; done |
|
||||
| 11 (close) | In this order: the GitHub mirror (push mirror from Gitea; GitHub Actions run the test matrix and build every release on Linux, Windows and a native macOS runner, so the `.dmg` and signed binaries come from CI; `install.sh`, `install.ps1`, the launcher's update check and the download buttons point to GitHub releases; the docs site keeps deploying from Gitea); custom installers for Windows, macOS and Linux (own UI instead of the stock NSIS pages, a drag-to-Applications `.dmg` with a background, an AppImage with a first-run desktop integration prompt); editor telemetry, logs and crash bundles sent to akarpov.ru with opt-in consent; last, telemetry and crash collection for exported games, sent to the developer's own endpoint or to an akarpov.ru account with a limited quota |
|
||||
| 13+ | Export/pak, networking/store, Android |
|
||||
|
||||
### Stage 9 complete
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package edit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"aego/engine"
|
||||
"aego/platform"
|
||||
)
|
||||
|
||||
func startBoot(verb string) {
|
||||
wd, _ := os.Getwd()
|
||||
exe, _ := os.Executable()
|
||||
gobin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
gobin = "not on PATH"
|
||||
}
|
||||
_ = platform.StartBoot(platform.DefaultBootPath(),
|
||||
"aego "+engine.Version+" "+runtime.GOOS+"/"+runtime.GOARCH+" "+verb,
|
||||
"exe "+exe,
|
||||
"args "+strings.Join(os.Args[1:], " "),
|
||||
"cwd "+wd,
|
||||
"go "+gobin,
|
||||
)
|
||||
}
|
||||
+6
-1
@@ -61,7 +61,8 @@ func Main(mods ...sdk.Module) {
|
||||
|
||||
func fail(err error) {
|
||||
os.Stderr.WriteString("aego-editor: " + err.Error() + "\n")
|
||||
if !interactive() {
|
||||
platform.Boot("fatal", err.Error())
|
||||
if !platform.ShowFatal("Aego", platform.FatalText("Aego", err.Error())) && !interactive() {
|
||||
platform.Alert("Aego", err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
@@ -112,6 +113,8 @@ func relInside(root, file string) (string, bool) {
|
||||
}
|
||||
|
||||
func Start(e Entry) error {
|
||||
defer platform.RecoverFatal("Aego")
|
||||
startBoot("edit")
|
||||
platform.SetAppIcon(brand.Window()...)
|
||||
markEntry(e.Verb)
|
||||
cfg := e.Config
|
||||
@@ -145,6 +148,7 @@ func Start(e Entry) error {
|
||||
cfg.OpenPath = rel
|
||||
}
|
||||
}
|
||||
platform.Boot("editor", p.Root)
|
||||
if bootstrap.NeedsNative(p) && !cfg.Headless && cfg.Frames == 0 && !cfg.Supervised {
|
||||
if goFound() {
|
||||
return supervise.Run(p, supervise.Options{
|
||||
@@ -153,6 +157,7 @@ func Start(e Entry) error {
|
||||
})
|
||||
}
|
||||
cfg.NativeNote = NoGoNote
|
||||
platform.Boot("native", NoGoNote)
|
||||
}
|
||||
return Run(p, cfg, e.Mods...)
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ func OpenLauncher(cfg Config) (*LaunchSession, error) {
|
||||
}
|
||||
ls := &LaunchSession{Engine: e}
|
||||
d := &launchDriver{s: ls, renderer: mod.Renderer(), log: lg.With(shell.ChannelEditor), images: map[string]launchImage{}}
|
||||
platform.Boot("launcher", "open")
|
||||
ls.UI = launcher.New(launcher.Options{
|
||||
Templates: launcherTemplates(),
|
||||
Examples: examples,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var boot struct {
|
||||
mu sync.Mutex
|
||||
f *os.File
|
||||
path string
|
||||
}
|
||||
|
||||
func DefaultBootPath() string {
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(dir, "aego", "logs", "boot.log")
|
||||
}
|
||||
|
||||
func StartBoot(path string, lines ...string) error {
|
||||
boot.mu.Lock()
|
||||
defer boot.mu.Unlock()
|
||||
if boot.f != nil {
|
||||
boot.f.Close()
|
||||
boot.f, boot.path = nil, ""
|
||||
}
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Rename(path, strings.TrimSuffix(path, ".log")+".prev.log")
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
boot.f, boot.path = f, path
|
||||
for _, l := range lines {
|
||||
writeBoot("start", l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BootPath() string {
|
||||
boot.mu.Lock()
|
||||
defer boot.mu.Unlock()
|
||||
return boot.path
|
||||
}
|
||||
|
||||
func Boot(stage, detail string) {
|
||||
boot.mu.Lock()
|
||||
defer boot.mu.Unlock()
|
||||
writeBoot(stage, detail)
|
||||
}
|
||||
|
||||
func writeBoot(stage, detail string) {
|
||||
if boot.f == nil {
|
||||
return
|
||||
}
|
||||
boot.f.WriteString(time.Now().Format("15:04:05.000") + " " + stage + ": " + detail + "\n")
|
||||
boot.f.Sync()
|
||||
}
|
||||
|
||||
func FatalText(title, msg string) string {
|
||||
text := title + " cannot start: " + msg
|
||||
if p := BootPath(); p != "" {
|
||||
text += "\n\nDetails: " + p
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func RecoverFatal(title string) {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprint(r)
|
||||
Boot("panic", msg)
|
||||
Boot("stack", string(debug.Stack()))
|
||||
text := FatalText(title, msg)
|
||||
if !ShowFatal(title, text) {
|
||||
fmt.Fprintln(os.Stderr, text)
|
||||
}
|
||||
os.Exit(2)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
func ShowFatal(title, msg string) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
mbIconError = 0x10
|
||||
mbSetForeground = 0x10000
|
||||
mbTopmost = 0x40000
|
||||
)
|
||||
|
||||
var messageBox = syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW")
|
||||
|
||||
func ShowFatal(title, msg string) bool {
|
||||
t, err := syscall.UTF16PtrFromString(strings.ReplaceAll(title, "\x00", ""))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
m, err := syscall.UTF16PtrFromString(strings.ReplaceAll(msg, "\x00", ""))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
r, _, _ := messageBox.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), mbIconError|mbSetForeground|mbTopmost)
|
||||
return r != 0
|
||||
}
|
||||
@@ -552,3 +552,36 @@ bool aego_set_icon(SDL_Window* w, SDL_Surface* s) {
|
||||
SDL_DestroySurface(s);
|
||||
return ok;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define AEGO_GLAPI __stdcall
|
||||
#else
|
||||
#define AEGO_GLAPI
|
||||
#endif
|
||||
|
||||
typedef const unsigned char*(AEGO_GLAPI* aego_getstring)(unsigned int);
|
||||
|
||||
const char* aego_gl_string(unsigned int name) {
|
||||
aego_getstring fn = (aego_getstring)SDL_GL_GetProcAddress("glGetString");
|
||||
if (!fn) {
|
||||
return "";
|
||||
}
|
||||
const unsigned char* v = fn(name);
|
||||
return v ? (const char*)v : "";
|
||||
}
|
||||
|
||||
const char* aego_gl_probe(SDL_Window* w) {
|
||||
static char buf[256];
|
||||
buf[0] = 0;
|
||||
SDL_GL_ResetAttributes();
|
||||
SDL_GLContext c = SDL_GL_CreateContext(w);
|
||||
if (!c) {
|
||||
return buf;
|
||||
}
|
||||
if (SDL_GL_MakeCurrent(w, c)) {
|
||||
SDL_snprintf(buf, sizeof buf, "%s (%s)", aego_gl_string(0x1F02), aego_gl_string(0x1F01));
|
||||
}
|
||||
SDL_GL_MakeCurrent(w, NULL);
|
||||
SDL_GL_DestroyContext(c);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -125,4 +125,7 @@ SDL_Surface* aego_icon_surface(int w, int h, const void* rgba);
|
||||
bool aego_icon_alternate(SDL_Surface* base, SDL_Surface* alt);
|
||||
bool aego_set_icon(SDL_Window* w, SDL_Surface* s);
|
||||
|
||||
const char* aego_gl_string(unsigned int name);
|
||||
const char* aego_gl_probe(SDL_Window* w);
|
||||
|
||||
#endif
|
||||
|
||||
+26
-6
@@ -63,10 +63,21 @@ func setMetadata(m platform.AppMetadata) {
|
||||
C.SDL_SetAppMetadata(name, ver, id)
|
||||
}
|
||||
|
||||
func glMessage(found string) string {
|
||||
if found == "" {
|
||||
found = "no OpenGL at all"
|
||||
}
|
||||
return "OpenGL 3.3 is required, the graphics driver provides " + found +
|
||||
". Install the driver from the GPU vendor; in a virtual machine or over Remote Desktop turn on 3D acceleration or run Aego on the host"
|
||||
}
|
||||
|
||||
func New() (*Platform, error) {
|
||||
setMetadata(platform.AppMeta())
|
||||
platform.Boot("sdl", "init")
|
||||
if !C.aego_init() {
|
||||
return nil, errs.New(errs.PlatformInit, "SDL_Init failed", log.F("sdl", lastError()))
|
||||
msg := lastError()
|
||||
platform.Boot("sdl", "init failed: "+msg)
|
||||
return nil, errs.New(errs.PlatformInit, "SDL_Init failed", log.F("sdl", msg))
|
||||
}
|
||||
v := C.aego_version()
|
||||
p := &Platform{
|
||||
@@ -82,6 +93,7 @@ func New() (*Platform, error) {
|
||||
},
|
||||
start: uint64(C.aego_ticks_ns()),
|
||||
}
|
||||
platform.Boot("sdl", "SDL "+p.info.Version+", video "+p.info.VideoDriver)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -112,23 +124,30 @@ func (p *Platform) CreateWindow(desc platform.WindowDesc) (platform.Window, erro
|
||||
}
|
||||
title := C.CString(desc.Title)
|
||||
defer C.free(unsafe.Pointer(title))
|
||||
platform.Boot("window", fmt.Sprintf("%dx%d hidden=%v", desc.Width, desc.Height, desc.Hidden))
|
||||
sw := C.aego_create_window(title, C.int(desc.Width), C.int(desc.Height),
|
||||
boolInt(desc.Resizable), boolInt(desc.Fullscreen), boolInt(desc.HighDPI))
|
||||
if sw == nil {
|
||||
return nil, errs.New(errs.PlatformWindow, "SDL_CreateWindow failed", log.F("sdl", lastError()))
|
||||
msg := lastError()
|
||||
platform.Boot("window", "failed: "+msg)
|
||||
return nil, errs.New(errs.PlatformWindow, "cannot open a window: "+msg, log.F("sdl", msg))
|
||||
}
|
||||
ctx := C.SDL_GL_CreateContext(sw)
|
||||
if ctx == nil {
|
||||
err := errs.New(errs.PlatformContext, "SDL_GL_CreateContext failed", log.F("sdl", lastError()))
|
||||
msg := lastError()
|
||||
found := C.GoString(C.aego_gl_probe(sw))
|
||||
platform.Boot("gl", "3.3 core context failed: "+msg+"; driver has "+found)
|
||||
C.SDL_DestroyWindow(sw)
|
||||
return nil, err
|
||||
return nil, errs.New(errs.PlatformContext, glMessage(found), log.F("sdl", msg))
|
||||
}
|
||||
if !C.SDL_GL_MakeCurrent(sw, ctx) {
|
||||
err := errs.New(errs.PlatformContext, "SDL_GL_MakeCurrent failed", log.F("sdl", lastError()))
|
||||
msg := lastError()
|
||||
platform.Boot("gl", "make current failed: "+msg)
|
||||
C.SDL_GL_DestroyContext(ctx)
|
||||
C.SDL_DestroyWindow(sw)
|
||||
return nil, err
|
||||
return nil, errs.New(errs.PlatformContext, "SDL_GL_MakeCurrent failed", log.F("sdl", msg))
|
||||
}
|
||||
platform.Boot("gl", C.GoString(C.aego_gl_string(0x1F02))+" ("+C.GoString(C.aego_gl_string(0x1F01))+")")
|
||||
w := &Window{p: p, win: sw, ctx: ctx, fullscreen: desc.Fullscreen, cursor: 255}
|
||||
w.SetVSync(desc.VSync)
|
||||
C.SDL_StopTextInput(sw)
|
||||
@@ -138,6 +157,7 @@ func (p *Platform) CreateWindow(desc platform.WindowDesc) (platform.Window, erro
|
||||
}
|
||||
w.refresh()
|
||||
p.win = w
|
||||
platform.Boot("window", "shown")
|
||||
return w, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user