Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9da9851538 | ||
|
|
4fd1e11741 | ||
|
|
8f9a0299dc | ||
|
|
ca3eedf9b0 | ||
|
|
4f0662eb3b | ||
|
|
9abf5c75ab | ||
|
|
96fef1f6a8 | ||
|
|
6230d30620 | ||
|
|
91ea202e74 | ||
|
|
6128c86685 | ||
|
|
2be6ce1d64 | ||
|
|
2e664ae508 | ||
|
|
aab6e0b0c9 | ||
|
|
9ba3e1631f | ||
|
|
4068e00375 | ||
|
|
914cf39150 | ||
|
|
dede4d8af3 | ||
|
|
b6dd46f523 | ||
|
|
9d32052624 | ||
|
|
e636801578 | ||
|
|
01fdeef7a8 | ||
|
|
71354f6a60 | ||
|
|
8ae6832503 | ||
|
|
183d75ed50 | ||
|
|
19edcbf9b2 | ||
|
|
8ed77a7638 | ||
|
|
ca3c9bf2e7 | ||
|
|
db59f117a4 | ||
|
|
0d2c20da26 | ||
|
|
dd41ff15a5 | ||
|
|
599e09a35d | ||
|
|
60614938be | ||
|
|
a60ca47d31 | ||
|
|
00ae2418a0 | ||
|
|
8fc9b4c287 | ||
|
|
b29934b51d | ||
|
|
c475b42e5d | ||
|
|
b103816400 | ||
|
|
487170dd9c | ||
|
|
57579356d6 | ||
|
|
d5377410e4 | ||
|
|
f6c83a6e0c | ||
|
|
8d7712d671 | ||
|
|
63980faccb | ||
|
|
0041e7e639 | ||
|
|
e334be569f | ||
|
|
b488958444 | ||
|
|
5875a65193 | ||
|
|
6ab66cb8ae | ||
|
|
f8fe54e9c5 | ||
|
|
e4326e7e7c | ||
|
|
6a22396467 | ||
|
|
21e03128a8 | ||
|
|
e5b1964f3e | ||
|
|
cffa66514b | ||
|
|
2744cff481 | ||
|
|
91052a54db | ||
|
|
7f4d87ef2c | ||
|
|
6cc0673082 | ||
|
|
ab9b003457 | ||
|
|
41d949717d | ||
|
|
69d19e501f | ||
|
|
cf68c71f05 | ||
|
|
08f0b15e1e | ||
|
|
17cb5e4291 | ||
|
|
eb9ac1e589 | ||
|
|
dae4bc19e1 |
+1
-1
@@ -507,7 +507,7 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
|
||||
| D291 | Playground runtime. One page holds the code editor, a Game View (`canvas[data-aego]`) and Console/Problems. The catalog `app/assembly/playground` is core, input, render2d, scripting, physics and debug (physics joins the playground graph, superseding D205 on that point; `render3d` stays out). Console examples (docs snippets, the donut) run headless as before. Scene examples come from `app/playground/examples`, a regular project (module `game`) embedded in the binary and checked by `aego check` in `make check-all`: its `project.aego` gives the project sections once at start, and each `scenes/<name>.aescene` with `scripts/<name>/` is one example. Run compiles the edited package in the same workspace, links a fresh script runtime and loads the scene through `sceneio` with a loader over an empty index, so only builtin assets resolve. One engine per page is built on the first Run on `platform/web` and `gpu/webgl2` and stays on `requestAnimationFrame`; Run and Stop swap the scene, the runtime and the module attachment of the main world. The page polls `poll` every 250 ms for log lines, faults and frame statistics (frame time, draw calls, WebGL calls of the last frame). The game owns the keyboard and pads while the canvas has focus, Escape included; a click elsewhere releases it. `bench` draws N sprites straight through the renderer for measurements and the screenshot golden, `loseContext` drives `WEBGL_lose_context`. Budgets are targets: `aego.wasm` ≤ 8 MB gzip (the site build fails above it), cold start ≤ 2 s on a laptop. `make playground-size` reports code size per package from the wasm name section (`tools/wasmsize`). `make web-smoke` runs Playwright with the system Chromium on SwiftShader: start-up time, a golden screenshot of `bench` 64, the same picture after a context loss, every scene example for 2 s without faults, and the 1000-sprite bench written to `.cache/web-bench.json`. |
|
||||
| D292 | Documentation IA. One registry feeds the site, the editor's docs panel and `aego docs export` (version 2, with groups). Sections and groups: Getting started; Guides (Editor, Aego Script, Scenes & Prefabs, Gameplay, Physics 2D, Rendering, Extending Aego, Tools & Agents); Reference (Script API, Components, Project settings, Diagnostics, CLI, Project). A root Markdown page declares `group:` (a guide group) and `order:`, children inherit the group of their root. The kernel lists only the module-independent groups; a module adds its own guide group from its root page with `group_title:` and `group_order:` (Physics 2D comes from `modules/physics`, so `engine/docs` never names a module and needs no literal waiver under D212). Reference groups are filled from the registry: script modules are roots of Script API ordered by the owning module's `DocsOrder`; components hang under one page per module, created only for modules with components and titled `module.Info.DisplayName` (the core module is "Core"); diagnostic codes under Diagnostics; documented project scripts under Project. Entry IDs are unchanged, so `ref:` links keep working. `aego docs check` fails on a root page without a guide group or an order and on a built-in module without a DisplayName. `module.DocSource.Dir`, taken from the module's package path (`Unwrap` sees through catalog wrappers), gives every page its repository path for "Edit this page"; `aego docs build` points those links at the checked-out branch (`master` when git is unavailable). gendocs renders sections with collapsible groups whose open state the browser remembers, the current page highlighted with its group and ancestors open, breadcrumbs (section, group, ancestors), an on-this-page list from the h2/h3 anchors, previous/next in navigation order, search grouped by section, a footer with the engine version, the commit and links to the source, the playground and the page's file, a landing page with three entry points and `docs/hero.png` when present, and `llms.txt`. A text outline of the navigation is the golden of the gendocs tests, for a fixture and for the real export. The playground and the site share one set of colour tokens. Amended in 9.5b3b: component pages come from the schema: a header with category, module, space, scene uniqueness, required components and the Create path; a field table with type, the default read from the registered default, range and unit, notes (editor or script visibility, read-only, Play only, saved, live, choice and bitset sources, enum values, gizmo) and the field doc; component methods (`manifest.CompMethods`) and signals as members (a method that shares its name with a field gets the ID and title `Name()`); the script events of the owning module, listed under Script API → Script events with owners from `module.Manifest.EventOwners`; related guides (pages linking to the component or its fields, and root guides of the same module); an empty Try it slot. Project settings has one page per project section with its fields and default block (`Section.DefaultValue`, `Section.DefaultBlock`; a block that encodes to nothing is left out, and the section page lists the owning module's guides as related) and a Player settings page built from the settings registry. CLI is the authored page `cli.md` in group `ref-cli`, the only reference group a page may declare; a test keeps it listing every command of `cmd/aego`. A script module `aego/<id>` belongs to module `<id>` when no owner is registered. The editor's docs panel shows the same sections and groups. Amended 2026-10-20 (9.7): entries and groups carry an editor icon name (`Entry.Icon`, `Group.Icon`, `module.Info.DocsIcon`, `group_icon:`), and gendocs builds one SVG sprite from the editor icon sources and fails on an unknown name; the site uses the editor theme, and code is highlighted at build time with the editor tokenizer and palette, identifiers linked to their entries. Short names resolve everywhere: `Registry.Aliases` maps a last segment that names one entry (`Body2D`, `ApplyForce`, `timer.Timer`) to it, used by `Registry.Link` in the docs panel and exported for gendocs. Getting started is the install page; the editor tour moved to the Editor group with its IDs. `module.Info.DocsGroup` documents a module's components in a guide group instead of under Components (the Script component is in Aego Script). Script API module pages list their functions with signatures above the sections, with a filter; a function without its own example links to the module's runnable example. A ```aes check block is compiled but not run: component pages end with "From a script" examples compiled against the full builtin catalog (`playground.NewWith`) by the playground tests. |
|
||||
| D293 | Docs session. `editor/core/docsession.Session` serves the real Inspector and Game View without the editor shell: one scene from bytes, its own `document.Manager` holding the edited document (the Inspector's `services.Documents`), a second manager with a document over the same scene for `play.Controller`, which swaps in its play proxy during Play, so the Inspector always edits authored values; a `selection.Service` keyed by that document; a minimal `context.Editor` (engine, renderer, loader, types, commands, problems, an in-memory clipboard) and `Unavailable` for asset and scene operations. History works as in the editor; a committed change saves the scene to text (`OnChange`) and restarts Play, keeping a pause; Reset reloads the original bytes. `app/docsview` hosts it: the sandbox catalog, `LoopScope` editor, one canvas with a toolbar (Play/Pause, Reset, Undo, Redo), Game View and Inspector side by side (stacked below 640 px), a script host that links a sandboxed runtime and attaches the modules to the play world, and a JSON bridge (`load`, `reset`, `set`, `undo`, `redo`, `state`, `log`). `cmd/aego-try` builds it as `try/aego-docs.wasm` (`js && aego_editor`). The graph excludes SDL, gl33, the shell, every panel but the two, and the script importer; `asset/pipeline` stays because `editor/core/context`, `assetops` and the preset importer import it, and the Inspector gets no pipeline. Module editor extensions (custom inspectors) are not loaded. |
|
||||
| D294 | Try it and the sandbox. A module's `res/docs/try/<Name>.aescene` becomes the try scene of the component whose ID ends in `.<Name>` (preferring the module's own), a guide page can name one with `try:` and `try_select:`; the registry carries `Try` and `TrySelect` and the site renders a Try it block when the playground is built: the scene text, Copy scene, Reset, and a button that lazily loads the one docs runtime per site, moves its single canvas into the block and loads the scene there, selecting the documented component; Inspector edits rewrite the shown text. `aego docs check` runs every try scene on the sandbox catalog for 60 headless frames and fails on a load error, a warning, a fault or a scene from a module outside the sandbox. Sandbox (playground and docs runtime): the catalog is the allowlist of host modules (core, input, render2d, scripting, physics, debug and, since 9.7, tween and render3d with the built-in meshes, whose OBJ importer is left out of web builds; no save, settings, spawn or audio), user files are in memory or localStorage only, there is no file or network access; scripts run with 2M instructions per call and a 64 MB VM heap (`vm.Options.MaxHeap`, checked in the allocator while the machine runs and reported as `CodeBudget`, garbage included until the next collection); a scene above 10 000 nodes stops the game with an error. `aego-docs.wasm` is held to 10 MB gzip by `make site` (`make try-size` reports it per package). Try scenes with scripts and the playground's Inspect toggle come later. Amended 2026-10-20 (9.7): a console run (`aes run`, the playground Run) attaches the sandbox modules to its world as Play does and steps it with `TickWorld` on a windowless engine, so tweens, timers, actions, handles in `yield` and physics behave as in Play; before, scripts were stepped by hand and module calls failed with AES6010. |
|
||||
| D294 | Try it and the sandbox. A module's `res/docs/try/<Name>.aescene` becomes the try scene of the component whose ID ends in `.<Name>` (preferring the module's own), a guide page can name one with `try:` and `try_select:`; the registry carries `Try` and `TrySelect` and the site renders a Try it block when the playground is built: the scene text, Copy scene, Reset, and a button that lazily loads the one docs runtime per site, moves its single canvas into the block and loads the scene there, selecting the documented component; Inspector edits rewrite the shown text. `aego docs check` runs every try scene on the sandbox catalog for 60 headless frames and fails on a load error, a warning, a fault or a scene from a module outside the sandbox. Sandbox (playground and docs runtime): the catalog is the allowlist of host modules (core, input, render2d, scripting, physics, debug and, since 9.7, tween and render3d with the built-in meshes, whose OBJ importer is left out of web builds; no save, settings, spawn or audio), user files are in memory or localStorage only, there is no file or network access; scripts run with 2M instructions per call and a 64 MB VM heap (`vm.Options.MaxHeap`, checked in the allocator while the machine runs and reported as `CodeBudget`, garbage included until the next collection); a scene above 10 000 nodes stops the game with an error. `aego-docs.wasm` is held to 10 MB gzip by `make site` (`make try-size` reports it per package). Try scenes with scripts and the playground's Inspect toggle come later. Amended 2026-10-20 (9.7): a console run (`aes run`, the playground Run) attaches the sandbox modules to its world as Play does and steps it with `TickWorld` on a windowless engine, so tweens, timers, actions, handles in `yield` and physics behave as in Play; before, scripts were stepped by hand and module calls failed with AES6010. Amended: a try scene may bring `<Name>.aes` and `<Name>.<prefab>.aeprefab` (id from its `.meta`). `aego docs export` compiles the script with the playground compiler into `TryCode` (base64 bytecode modules); the page passes code and prefabs to `load`, the docs runtime publishes the script types before parsing the scene and mounts the prefabs into its loader from memory. The sandbox includes `spawn` and `savegame` with an in-memory save store, and a play controller whose scene has no asset id reloads that scene for `save.Load`. Every component has a try scene; `TestTryScenesRun` runs each through `load` for 400 frames. |
|
||||
| D295 | Web export. `aego export --target web` (`js/wasm`, shown as `web`) builds `cmd/aego-runtime` with `GOOS=js GOARCH=wasm CGO_ENABLED=0` and the runtime tag, release with `-trimpath -s -w`, and packs `index.html`, `loader.js` (both embedded in `app/export/web`), the toolchain's `wasm_exec.js` with its license, `game.wasm` and `data/` with the 9.5a manifest and artifacts plus `files.json`. The page script loads the data, not Go: it fetches `files.json`, then the wasm, the manifest and the artifacts six at a time with one progress bar over the uncompressed sizes, and hands the bytes to the runtime in `globalThis.aegoData`; `runtime/player.Main` under js mounts them with `vfs/memfs` after checking every manifest entry and size, as `exportfs` does on desktop. Under js the player has no log or crash files and no signal handling (`hostFiles`); the profile and saves go through `platform.UserFiles()` (localStorage). URL parameters replace the command-line flags; `globalThis.aegoGame` exposes the frame count and profile file access for tests. Files that compress by more than 10% get `.gz` (and `.br` when `brotli` exists); `export.aetext` records `binaryGzip`. Web-specific checks: EXP010 (asset kind the web runtime cannot load, none yet), EXP011 (native module must build without cgo). `make site` exports the platformer to `site/demo/`, the landing page links it (`aego docs build --demo`), and fails when its `game.wasm` is over 8 MB gzip; `make web-smoke` runs it for 120 frames against a screenshot golden and checks that a profile file survives a reload. `make test-wasm` builds the web runtime and rejects native, pipeline and editor packages in its graph. |
|
||||
|
||||
### 3.21 Stage 9.6 — Agent tooling
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"aego/modules/physics"
|
||||
"aego/modules/render2d"
|
||||
"aego/modules/render3d"
|
||||
"aego/modules/savegame"
|
||||
"aego/modules/scripting"
|
||||
"aego/modules/spawn"
|
||||
"aego/modules/tween"
|
||||
"aego/script/stdlib"
|
||||
"aego/script/vm"
|
||||
@@ -29,6 +31,8 @@ func Catalog() assembly.Catalog {
|
||||
{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }},
|
||||
{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }},
|
||||
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
|
||||
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
|
||||
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aego/contracts/persistence"
|
||||
"aego/engine"
|
||||
"aego/platform"
|
||||
"aego/save"
|
||||
)
|
||||
|
||||
// UseMemorySaves gives e a save store kept in memory, for runs in a browser page that must not touch the visitor's storage.
|
||||
func UseMemorySaves(e *engine.Engine) {
|
||||
e.SetService(persistence.StoreID, save.NewStoreFS("saves", &memUser{files: map[string][]byte{}}))
|
||||
}
|
||||
|
||||
type memUser struct {
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
|
||||
func (m *memUser) WriteFile(p string, data []byte) error {
|
||||
m.files[path.Clean(p)] = append([]byte(nil), data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Remove(p string) error {
|
||||
delete(m.files, path.Clean(p))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return platform.UserEntry{}, fs.ErrNotExist
|
||||
}
|
||||
return platform.UserEntry{Name: path.Base(p), Size: int64(len(data)), Mod: time.Time{}}, nil
|
||||
}
|
||||
|
||||
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
|
||||
prefix := path.Clean(dir) + "/"
|
||||
var out []platform.UserEntry
|
||||
for p, data := range m.files {
|
||||
if rest, ok := strings.CutPrefix(p, prefix); ok && !strings.Contains(rest, "/") {
|
||||
out = append(out, platform.UserEntry{Name: rest, Size: int64(len(data))})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"aego/asset"
|
||||
"aego/asset/loader"
|
||||
"aego/core/ids"
|
||||
"aego/script/bytecode"
|
||||
"aego/vfs"
|
||||
"aego/vfs/memfs"
|
||||
)
|
||||
|
||||
// EncodeTry packs the compiled script of a try scene for a docs page: base64 modules joined by commas.
|
||||
func EncodeTry(mods []*bytecode.Module) string {
|
||||
parts := make([]string, len(mods))
|
||||
for i, m := range mods {
|
||||
parts[i] = base64.StdEncoding.EncodeToString(bytecode.Encode(m))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func DecodeTry(code string) ([]*bytecode.Module, error) {
|
||||
if code == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var mods []*bytecode.Module
|
||||
for _, part := range strings.Split(code, ",") {
|
||||
b, err := base64.StdEncoding.DecodeString(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := bytecode.Decode(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mods = append(mods, m)
|
||||
}
|
||||
return mods, nil
|
||||
}
|
||||
|
||||
// MountTry gives the loader the prefabs of a try scene, keyed by asset id; none unmounts them.
|
||||
func MountTry(l *loader.Loader, prefabs map[string]string) error {
|
||||
idx := asset.NewIndex()
|
||||
files := map[string][]byte{}
|
||||
for key, text := range prefabs {
|
||||
id, err := ids.ParseAssetID(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := id.String() + ".aeprefab"
|
||||
files[p] = []byte(text)
|
||||
idx.Put(asset.Entry{ID: id, Kind: asset.ArtPrefab, Name: p, Source: p,
|
||||
Artifacts: []asset.Artifact{{Kind: asset.ArtPrefab, Path: "try://" + p}}})
|
||||
}
|
||||
fs, err := vfs.New(vfs.Mount{Scheme: "try", ReadOnly: true, Source: memfs.New(files)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.Mount(idx, fs)
|
||||
}
|
||||
@@ -21,7 +21,8 @@ import (
|
||||
"aego/script/workspace"
|
||||
)
|
||||
|
||||
func build(p *project.Project, mods []module.Module, diagnose func(string, *docs.DiagInfo)) (*docs.Registry, *workspace.Workspace, error) {
|
||||
// build assembles the registry; with pg it also compiles the diagnostics examples and the try scripts.
|
||||
func build(p *project.Project, mods []module.Module, pg *playground.Playground) (*docs.Registry, *workspace.Workspace, error) {
|
||||
types, err := assembly.NewTypes()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -42,7 +43,10 @@ func build(p *project.Project, mods []module.Module, diagnose func(string, *docs
|
||||
return nil, nil, err
|
||||
}
|
||||
src := docs.SourcesOf(bm, types.Schema(), man)
|
||||
src.Diagnose = diagnose
|
||||
if pg != nil {
|
||||
src.Diagnose = diagnoser(pg)
|
||||
src.CompileTry = pg.TryCode
|
||||
}
|
||||
var ws *workspace.Workspace
|
||||
if p != nil {
|
||||
ws, err = workspace.Open(workspace.Config{Root: p.Root, Module: p.Module, Scripts: p.ScriptsDir(), Bindings: reg.Table(), Docs: bm})
|
||||
@@ -67,13 +71,13 @@ func Registry(p *project.Project, mods []module.Module) (*docs.Registry, error)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// ExportRegistry is Registry with the diagnostics examples compiled for their messages and positions.
|
||||
// ExportRegistry is Registry with the diagnostics examples compiled for their messages and positions, and the try scripts compiled.
|
||||
func ExportRegistry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
||||
pg, err := playground.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, _, err := build(p, mods, diagnoser(pg))
|
||||
r, _, err := build(p, mods, pg)
|
||||
return r, err
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -41,9 +41,16 @@ func CheckTry(reg *docs.Registry, w io.Writer) int {
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
switch res := pg.PlayScene(e.ID, []byte(e.Try)); {
|
||||
var code string
|
||||
var err error
|
||||
if e.TryScript != "" {
|
||||
code, err = pg.TryCode(e.TryScript)
|
||||
}
|
||||
switch res := pg.PlayScene(e.ID, []byte(e.Try), code, e.TryPrefabs); {
|
||||
case !sandbox[e.ID]:
|
||||
add("belongs to a module the docs runtime does not include")
|
||||
case err != nil:
|
||||
add("script does not compile: " + err.Error())
|
||||
case !res.Started:
|
||||
add("does not load: " + res.Error)
|
||||
default:
|
||||
|
||||
+4
-1
@@ -75,6 +75,7 @@ type App struct {
|
||||
ui *core.Context
|
||||
clip *docsession.Clipboard
|
||||
session *docsession.Session
|
||||
scripts *scriptHost
|
||||
insp *inspector.Panel
|
||||
game *gameview.Panel
|
||||
list *draw.List
|
||||
@@ -143,15 +144,17 @@ func New(opt Options) (*App, error) {
|
||||
return nil, err
|
||||
}
|
||||
a.e.SetService(loader.ServiceID, a.loader)
|
||||
playmods.UseMemorySaves(a.e)
|
||||
a.e.SetService(renderhost.ServiceID, a.renderer)
|
||||
if err := a.man.Init(&module.InitContext{Engine: a.e, Assets: a.loader, Log: a.log, Editor: true, Sections: sections}); err != nil {
|
||||
a.Close()
|
||||
return nil, err
|
||||
}
|
||||
a.initUI()
|
||||
a.scripts = &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log}
|
||||
a.session = docsession.New(docsession.Options{
|
||||
Engine: a.e, Renderer: a.renderer, Loader: a.loader, Types: a.types, Sections: sections,
|
||||
Scripts: &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log},
|
||||
Scripts: a.scripts,
|
||||
UI: a.ui,
|
||||
Clipboard: a.clip,
|
||||
Log: a.log,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -46,11 +47,22 @@ func errorsOf(a *App) []string {
|
||||
|
||||
func TestTryScenesRun(t *testing.T) {
|
||||
a := newHeadless(t)
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range tries(t) {
|
||||
if err := a.session.Load([]byte(e.Try), e.TrySelect); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
var code string
|
||||
if e.TryScript != "" {
|
||||
if code, err = p.TryCode(e.TryScript); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
}
|
||||
}
|
||||
if err := a.Step(60); err != nil {
|
||||
req, _ := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs})
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatalf("%s: %s", e.ID, r.Error)
|
||||
}
|
||||
if err := a.Step(400); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
}
|
||||
st := a.session.State()
|
||||
|
||||
+18
-6
@@ -6,16 +6,19 @@ import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/editor/core/docsession"
|
||||
"aego/scene/sceneio"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Component string `json:"component"`
|
||||
Field string `json:"field"`
|
||||
Value float64 `json:"value"`
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Code string `json:"code"`
|
||||
Prefabs map[string]string `json:"prefabs"`
|
||||
Component string `json:"component"`
|
||||
Field string `json:"field"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
@@ -37,7 +40,16 @@ func (a *App) Call(method string, payload []byte) Reply {
|
||||
var err error
|
||||
switch method {
|
||||
case "load":
|
||||
err = s.Load([]byte(req.Scene), req.Select)
|
||||
mods, derr := playmods.DecodeTry(req.Code)
|
||||
if err = derr; err == nil {
|
||||
err = a.scripts.use(mods)
|
||||
}
|
||||
if err == nil {
|
||||
err = playmods.MountTry(a.loader, req.Prefabs)
|
||||
}
|
||||
if err == nil {
|
||||
err = s.Load([]byte(req.Scene), req.Select)
|
||||
}
|
||||
case "reset":
|
||||
err = s.Reset()
|
||||
case "text":
|
||||
|
||||
@@ -22,6 +22,15 @@ type scriptHost struct {
|
||||
mods []*bytecode.Module
|
||||
}
|
||||
|
||||
// use makes mods the scripts of the next scene and publishes their types, so the scene keeps their fields.
|
||||
func (h *scriptHost) use(mods []*bytecode.Module) error {
|
||||
if err := scriptruntime.Publish(h.types.Schema(), mods, h.reg.Surface()); err != nil {
|
||||
return err
|
||||
}
|
||||
h.mods = mods
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *scriptHost) Errors() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ func newGame(cfg GameConfig, types *scene.Types, reg *bind.Registry, man *module
|
||||
return nil, err
|
||||
}
|
||||
e.SetService(loader.ServiceID, g.loader)
|
||||
playmods.UseMemorySaves(e)
|
||||
e.SetService(renderhost.ServiceID, g.renderer)
|
||||
e.SetView(frame.ServiceID, g.runner)
|
||||
e.MainWorld().Recorder = recorder.New(recordDepth)
|
||||
|
||||
+31
-2
@@ -1,10 +1,13 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/script/bytecode"
|
||||
"aego/script/diag"
|
||||
)
|
||||
@@ -110,13 +113,39 @@ func (p *Playground) LoseContext(lose bool) bool {
|
||||
return p.game != nil && p.game.LoseContext(lose)
|
||||
}
|
||||
|
||||
func (p *Playground) PlayScene(name string, src []byte) PlayResult {
|
||||
func (p *Playground) PlayScene(name string, src []byte, code string, prefabs map[string]string) PlayResult {
|
||||
mods, err := playmods.DecodeTry(code)
|
||||
if err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
g, err := p.host()
|
||||
if err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
if err := g.Play(name, src, nil); err != nil {
|
||||
if err := playmods.MountTry(g.loader, prefabs); err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
if err := g.Play(name, src, mods); err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
return PlayResult{Started: true}
|
||||
}
|
||||
|
||||
// TryCode compiles the script of a try scene into the form DecodeTry reads.
|
||||
func (p *Playground) TryCode(script string) (string, error) {
|
||||
placed, err := place(map[string]string{"main.aes": script})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.ws.Sync(placed)
|
||||
gen, ds := p.ws.Build()
|
||||
if len(gen.Failed) > 0 || diag.HasErrors(ds) {
|
||||
for _, d := range diagnostics(gen.Sources, ds) {
|
||||
if d.Severity == "error" {
|
||||
return "", fmt.Errorf("%s:%d:%d: %s", d.File, d.Line, d.Col, d.Message)
|
||||
}
|
||||
}
|
||||
return "", errors.New("the script does not build")
|
||||
}
|
||||
return playmods.EncodeTry(ordered(gen.Modules)), nil
|
||||
}
|
||||
|
||||
@@ -75,12 +75,18 @@ func TestSandboxHeapLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxHasNoSaves(t *testing.T) {
|
||||
// The sandbox has aego/save for the Persist demo; its saves stay in memory, never in the visitor's storage.
|
||||
func TestSandboxSavesStayInMemory(t *testing.T) {
|
||||
p := newHeadless(t)
|
||||
for _, d := range p.Compile(map[string]string{"main.aes": saveScript}) {
|
||||
res := p.Run(map[string]string{"main.aes": saveScript}, 10)
|
||||
for _, d := range res.Diagnostics {
|
||||
if d.Severity == "error" {
|
||||
return
|
||||
t.Fatalf("aego/save does not compile in the sandbox: %s", d.Message)
|
||||
}
|
||||
}
|
||||
for _, l := range res.Log {
|
||||
if strings.Contains(l.Text, "saves are not available") {
|
||||
t.Fatal("the sandbox has no save store")
|
||||
}
|
||||
}
|
||||
t.Fatal("aego/save compiled in the sandbox")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Aego Playground</title>
|
||||
<meta name="description" content="Write Aego Script in the browser and run it: console examples and small scenes, with problems and output next to the code.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Aego">
|
||||
<meta property="og:title" content="Aego Playground">
|
||||
<meta property="og:description" content="Write Aego Script in the browser and run it: console examples and small scenes, with problems and output next to the code.">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="play.css?v=dev">
|
||||
</head>
|
||||
|
||||
@@ -222,6 +222,7 @@ function run() {
|
||||
}
|
||||
|
||||
function startBench(count) {
|
||||
gamePane.hidden = false;
|
||||
const res = call("bench", { count, animate: true });
|
||||
if (!res.started) {
|
||||
setStatus(res.error || "bench did not start", true);
|
||||
@@ -283,6 +284,7 @@ function syncScroll() {
|
||||
function selectExample(ex) {
|
||||
current = ex;
|
||||
framesLabel.hidden = !!(ex && ex.scene);
|
||||
gamePane.hidden = !!(ex && !ex.scene);
|
||||
if (!pollTimer) {
|
||||
gameHint.hidden = false;
|
||||
gameHint.textContent = ex && ex.scene ? "Press Run to play this scene" : "Console example: output goes to Console";
|
||||
|
||||
+14
-10
@@ -295,16 +295,20 @@ func (c *Controller) SceneID() ids.AssetID {
|
||||
|
||||
func (c *Controller) LoadScene(id ids.AssetID) error {
|
||||
ctx := c.ctx
|
||||
if ctx.Pipeline == nil {
|
||||
return errs.New(errs.EditorDocument, "the asset pipeline is not available")
|
||||
}
|
||||
rel, ok := ctx.Pipeline.PathOf(id)
|
||||
if !ok {
|
||||
return errs.New(errs.EditorDocument, "the saved scene is not in the project")
|
||||
}
|
||||
d, err := ctx.Docs.Open(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
d := ctx.Docs.Get(c.doc)
|
||||
// a scene that is not an asset, such as a docs Try it scene, saves with a zero id and reloads itself
|
||||
if !id.IsZero() || d == nil {
|
||||
if ctx.Pipeline == nil {
|
||||
return errs.New(errs.EditorDocument, "the asset pipeline is not available")
|
||||
}
|
||||
rel, ok := ctx.Pipeline.PathOf(id)
|
||||
if !ok {
|
||||
return errs.New(errs.EditorDocument, "the saved scene is not in the project")
|
||||
}
|
||||
var err error
|
||||
if d, err = ctx.Docs.Open(rel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
held := c.held
|
||||
if err := c.Start(d); err != nil {
|
||||
|
||||
@@ -94,3 +94,10 @@ func TestScriptDocsNeedRunBlocks(t *testing.T) {
|
||||
t.Fatalf("a project page is held to the run block rule: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarySkipsImages(t *testing.T) {
|
||||
got := summary("# Editor\n\n\n\nThe editor has workspaces.\n")
|
||||
if got != "The editor has workspaces." {
|
||||
t.Fatalf("summary = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ func summary(md string) string {
|
||||
continue
|
||||
}
|
||||
t := strings.TrimSpace(l)
|
||||
if fence || t == "" || strings.HasPrefix(t, "#") || strings.HasPrefix(t, "|") {
|
||||
if fence || t == "" || strings.HasPrefix(t, "#") || strings.HasPrefix(t, "|") || strings.HasPrefix(t, "![") && strings.HasSuffix(t, ")") {
|
||||
continue
|
||||
}
|
||||
return unwrap(trimMarker(t))
|
||||
|
||||
+55
-1
@@ -5,6 +5,7 @@ package docs
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -50,6 +51,7 @@ const (
|
||||
unknownOrder = 1000
|
||||
TryDir = "try"
|
||||
TryExt = ".aescene"
|
||||
TryScriptExt = ".aes"
|
||||
)
|
||||
|
||||
var kindNames = [...]string{"page", "module", "function", "type", "field", "method", "signal", "code", "const", "enum"}
|
||||
@@ -107,7 +109,12 @@ type Entry struct {
|
||||
Related []string
|
||||
Try string
|
||||
TrySelect string
|
||||
Icon string
|
||||
// TryScript is the script next to the try scene; TryCode is its compiled bytecode, base64 modules joined by commas.
|
||||
TryScript string `json:",omitempty"`
|
||||
TryCode string `json:",omitempty"`
|
||||
// TryPrefabs are the prefabs next to the try scene, by asset id.
|
||||
TryPrefabs map[string]string `json:",omitempty"`
|
||||
Icon string
|
||||
// EditorOnly pages are shown in the editor's docs panel and left out of the site.
|
||||
EditorOnly bool `json:",omitempty"`
|
||||
Diag *DiagInfo
|
||||
@@ -156,6 +163,8 @@ type Sources struct {
|
||||
Settings []settings.Desc
|
||||
EventOwners map[string]string
|
||||
Diagnose func(code string, d *DiagInfo)
|
||||
// CompileTry turns the script of a try scene into the bytecode the docs runtime loads.
|
||||
CompileTry func(script string) (string, error)
|
||||
}
|
||||
|
||||
type pageMeta struct {
|
||||
@@ -269,6 +278,7 @@ func Assemble(src Sources) *Registry {
|
||||
r.pages(d)
|
||||
r.tries(d)
|
||||
}
|
||||
r.compileTries(src.CompileTry)
|
||||
r.index()
|
||||
r.related()
|
||||
return r
|
||||
@@ -854,6 +864,49 @@ func (r *Registry) pageTry(src module.DocSource, rec record) {
|
||||
return
|
||||
}
|
||||
r.entries[i].Try, r.entries[i].TrySelect = string(data), rec.trySelect
|
||||
r.tryExtras(&r.entries[i], src, rec.try)
|
||||
}
|
||||
|
||||
var metaID = regexp.MustCompile(`\bid="([0-9a-f]{32})"`)
|
||||
|
||||
// tryExtras reads the script X.aes and the prefabs X.<name>.aeprefab, ids from their .meta, next to the try scene X.aescene.
|
||||
func (r *Registry) tryExtras(e *Entry, src module.DocSource, scenePath string) {
|
||||
base := strings.TrimSuffix(path.Clean(scenePath), TryExt)
|
||||
if data, err := fs.ReadFile(src.FS, base+TryScriptExt); err == nil {
|
||||
e.TryScript = string(data)
|
||||
}
|
||||
files, _ := fs.Glob(src.FS, base+".*.aeprefab")
|
||||
for _, f := range files {
|
||||
text, err := fs.ReadFile(src.FS, f)
|
||||
meta, merr := fs.ReadFile(src.FS, f+".meta")
|
||||
m := metaID.FindSubmatch(meta)
|
||||
if err != nil || merr != nil || m == nil {
|
||||
r.problems = append(r.problems, Problem{ID: e.ID, Error: true, Message: "try prefab " + f + " needs a .meta with its id"})
|
||||
continue
|
||||
}
|
||||
if e.TryPrefabs == nil {
|
||||
e.TryPrefabs = map[string]string{}
|
||||
}
|
||||
e.TryPrefabs[string(m[1])] = string(text)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) compileTries(compile func(string) (string, error)) {
|
||||
if compile == nil {
|
||||
return
|
||||
}
|
||||
for i := range r.entries {
|
||||
e := &r.entries[i]
|
||||
if e.TryScript == "" {
|
||||
continue
|
||||
}
|
||||
code, err := compile(e.TryScript)
|
||||
if err != nil {
|
||||
r.problems = append(r.problems, Problem{ID: e.ID, Error: true, Message: "try script does not compile: " + err.Error()})
|
||||
continue
|
||||
}
|
||||
e.TryCode = code
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) componentNamed(short, owner string) (int, bool) {
|
||||
@@ -892,5 +945,6 @@ func (r *Registry) tries(src module.DocSource) {
|
||||
}
|
||||
e := &r.entries[i]
|
||||
e.Try, e.TrySelect = string(data), e.ID
|
||||
r.tryExtras(e, src, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ Each frame the brain picks one virtual camera per output:
|
||||
|
||||
The pose comes from the virtual camera's node: its interpolated position and rotation, and [Zoom](ref:camera.VirtualCamera2D.Zoom), where 0 keeps the zoom of the Camera 2D. A Camera Follow 2D on the same node replaces the position. The node's transform is never changed; the camera only moves the view.
|
||||
|
||||
In Try it, select **Close** and set its Priority to 1: the view blends to it. Set it back to -1 and the view blends back.
|
||||
In Try it, the Director script raises **Close**, then **Tilt**, for two seconds each, and the view blends to them and back. Change Zoom, Blend Time or Blend Curve of Close in the Inspector and watch the next blend; the script is under the scene.
|
||||
|
||||
## Blends
|
||||
|
||||
@@ -118,6 +118,8 @@ In Try it, the purple area in the middle of the ramp is the zone: the view blend
|
||||
|
||||
---
|
||||
id: camera/shake
|
||||
try: try/CameraShake2D.aescene
|
||||
try_select: camera.CameraShake2D
|
||||
icon: comp.shake
|
||||
parent: camera
|
||||
order: 5
|
||||
@@ -125,6 +127,8 @@ title: Shake
|
||||
---
|
||||
# Shake
|
||||
|
||||
In Try it, the crate drops every two seconds and its script calls `camera.Shake(0.9)` when it lands. Raise Max Angle or lower Exponent and Decay on the camera to feel the difference.
|
||||
|
||||
Shake is driven by trauma, a value from 0 to 1 per output. `camera.Shake(0.4)` adds trauma, which fades by [Decay](ref:camera.CameraShake2D.Decay) per second. The view moves by up to [Max Offset](ref:camera.CameraShake2D.MaxOffset) and turns by up to [Max Angle](ref:camera.CameraShake2D.MaxAngle), scaled by trauma to the power [Exponent](ref:camera.CameraShake2D.Exponent), so small hits stay subtle. Without a Camera Shake 2D on the active camera the defaults are used.
|
||||
|
||||
The motion comes from deterministic noise, so a replay shakes the same way, and it never touches the node's transform or the game's random numbers.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"aego/camera"
|
||||
"aego/physics"
|
||||
)
|
||||
|
||||
// Drop lifts the crate every few seconds; each landing shakes the camera.
|
||||
script Drop {
|
||||
@export var Every float = 2
|
||||
@export(min=0, max=1)
|
||||
var Hit float = 0.9
|
||||
|
||||
var t float
|
||||
var start Vec2
|
||||
|
||||
func Ready() {
|
||||
start = node.Transform2D().Position
|
||||
}
|
||||
|
||||
func Update(dt float) {
|
||||
t += dt
|
||||
if t >= Every {
|
||||
t = 0
|
||||
body := node.Get<Body2D>()
|
||||
body.Teleport(start, 0)
|
||||
body.SetVelocity(Vec2{0, 0})
|
||||
}
|
||||
}
|
||||
|
||||
func OnCollisionEnter(other NodeRef, c physics.Contact) {
|
||||
camera.Shake(Hit)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
package demo
|
||||
|
||||
// Director raises Close, then Tilt, for Hold seconds each; the view blends between them.
|
||||
script Director {
|
||||
@export var Close ComponentRef<VirtualCamera2D>
|
||||
@export var Tilt ComponentRef<VirtualCamera2D>
|
||||
@export var Hold float = 2
|
||||
|
||||
var t float
|
||||
var shot int
|
||||
|
||||
func Update(dt float) {
|
||||
t += dt
|
||||
if t < Hold {
|
||||
return
|
||||
}
|
||||
t = 0
|
||||
shot = (shot + 1) % 4
|
||||
Close.Priority = -1
|
||||
Tilt.Priority = -1
|
||||
if shot == 1 {
|
||||
Close.Priority = 1
|
||||
} else if shot == 3 {
|
||||
Tilt.Priority = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
scene version=1 {
|
||||
node "d5200000000000000000000000000001" name="Close" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[6, 0]
|
||||
rotation=0.08
|
||||
position=[-6, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "camera.VirtualCamera2D" version=1 {
|
||||
priority=-1
|
||||
output=""
|
||||
zoom=96
|
||||
zoom=110
|
||||
blendTime=1
|
||||
blendCurve={ ease=inOutQuad }
|
||||
timeDomain=unscaled
|
||||
@@ -17,6 +17,24 @@ scene version=1 {
|
||||
snapToPixels=false
|
||||
}
|
||||
}
|
||||
node "d5200000000000000000000000000009" name="Tilt" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[6, -0.4]
|
||||
rotation=0.14
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "camera.VirtualCamera2D" version=1 {
|
||||
priority=-1
|
||||
output=""
|
||||
zoom=80
|
||||
blendTime=1.5
|
||||
blendCurve={ ease=outBack }
|
||||
timeDomain=unscaled
|
||||
useBounds=false
|
||||
bounds=[0, 0, 0, 0]
|
||||
snapToPixels=false
|
||||
}
|
||||
}
|
||||
node "d5200000000000000000000000000002" name="Camera" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
@@ -160,4 +178,18 @@ scene version=1 {
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
node "d5200000000000000000000000000010" name="Director" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.Script" version=1 {
|
||||
script "game.demo.Director" enabled=true {
|
||||
Close=node("d5200000000000000000000000000001")
|
||||
Tilt=node("d5200000000000000000000000000009")
|
||||
Hold=2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
prefab version=1 {
|
||||
node "d5600000000000000000000000000101" name="Lamp" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.Script" version=1 {
|
||||
script "game.demo.Spin" enabled=true {
|
||||
Speed=2
|
||||
}
|
||||
}
|
||||
node "d5600000000000000000000000000102" name="Post" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 1.2]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.SpriteRenderer" version=1 {
|
||||
sprite=asset("ae900000000000000000000000000010")
|
||||
layer=0
|
||||
order=0
|
||||
tint=#6b7280ff
|
||||
origin=[0.5, 0.5]
|
||||
size=[0.3, 2.4]
|
||||
flipX=false
|
||||
flipY=false
|
||||
blend=premultiplied
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
node "d5600000000000000000000000000103" name="Head" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.SpriteRenderer" version=1 {
|
||||
sprite=asset("ae900000000000000000000000000010")
|
||||
layer=0
|
||||
order=0
|
||||
tint=#facc15ff
|
||||
origin=[0.5, 0.5]
|
||||
size=[1.2, 1.2]
|
||||
flipX=false
|
||||
flipY=false
|
||||
blend=premultiplied
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
meta version=1 {
|
||||
id="d5600000000000000000000000000f01"
|
||||
importer="prefab"
|
||||
importerVersion=2
|
||||
settings {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package demo
|
||||
|
||||
// Spin turns the Head child of a lamp; each instance overrides Speed.
|
||||
script Spin {
|
||||
@export(unit="rad/s")
|
||||
var Speed float = 2
|
||||
|
||||
func Update(dt float) {
|
||||
head := node.Child("Head")
|
||||
tr := head.Transform2D()
|
||||
tr.Rotation += Speed * dt
|
||||
head.SetTransform2D(tr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
scene version=1 {
|
||||
node "d5600000000000000000000000000001" prefab=asset("d5600000000000000000000000000f01") {
|
||||
id "d5600000000000000000000000000101" node=node("d5600000000000000000000000000001") {}
|
||||
override "d5600000000000000000000000000101" prop="node.name" value="Red" {}
|
||||
override "d5600000000000000000000000000101" prop="transform.position" value=[-2, 0] {}
|
||||
override "d5600000000000000000000000000103" prop="engine.SpriteRenderer.tint" value=#ef4444ff {}
|
||||
override "d5600000000000000000000000000101" prop="script/game.demo.Spin.Speed" value=-3 {}
|
||||
}
|
||||
node "d5600000000000000000000000000002" prefab=asset("d5600000000000000000000000000f01") {
|
||||
id "d5600000000000000000000000000101" node=node("d5600000000000000000000000000002") {}
|
||||
override "d5600000000000000000000000000101" prop="node.name" value="Lamp" {}
|
||||
override "d5600000000000000000000000000101" prop="transform.position" value=[-6, 0] {}
|
||||
}
|
||||
node "d5600000000000000000000000000003" prefab=asset("d5600000000000000000000000000f01") {
|
||||
id "d5600000000000000000000000000101" node=node("d5600000000000000000000000000003") {}
|
||||
override "d5600000000000000000000000000101" prop="node.name" value="Big" {}
|
||||
override "d5600000000000000000000000000101" prop="transform.position" value=[2, -0.6] {}
|
||||
override "d5600000000000000000000000000101" prop="transform.scale" value=[1.5, 1.5] {}
|
||||
override "d5600000000000000000000000000101" prop="script/game.demo.Spin.Speed" value=0.8 {}
|
||||
}
|
||||
node "d5600000000000000000000000000004" prefab=asset("d5600000000000000000000000000f01") {
|
||||
id "d5600000000000000000000000000101" node=node("d5600000000000000000000000000004") {}
|
||||
override "d5600000000000000000000000000101" prop="node.name" value="Blue" {}
|
||||
override "d5600000000000000000000000000101" prop="transform.position" value=[6, 0] {}
|
||||
override "d5600000000000000000000000000103" prop="engine.SpriteRenderer.tint" value=#3b82f6ff {}
|
||||
override "d5600000000000000000000000000101" prop="script/game.demo.Spin.Speed" value=5 {}
|
||||
}
|
||||
node "d5600000000000000000000000000010" name="Floor" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 2.6]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.SpriteRenderer" version=1 {
|
||||
sprite=asset("ae900000000000000000000000000010")
|
||||
layer=0
|
||||
order=0
|
||||
tint=#374151ff
|
||||
origin=[0.5, 0.5]
|
||||
size=[18, 0.4]
|
||||
flipX=false
|
||||
flipY=false
|
||||
blend=premultiplied
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
node "d5600000000000000000000000000011" name="Camera" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.Camera2D" version=1 {
|
||||
zoom=40
|
||||
viewport=[0, 0, 1, 1]
|
||||
mask=-1
|
||||
order=0
|
||||
clear=true
|
||||
clearColor=#111827ff
|
||||
scale=fitVirtual
|
||||
virtual=[960, 540]
|
||||
active=true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
id: save
|
||||
try: try/Persist.aescene
|
||||
try_select: save.Persist
|
||||
icon: panel.saves
|
||||
group: gameplay
|
||||
title: Saving games
|
||||
@@ -9,6 +11,8 @@ order: 6
|
||||
|
||||
A save holds what you mark as persistent, not the whole scene. Loading a save reloads its scene from the project and then applies the save on top.
|
||||
|
||||
In Try it, the Checkpoint script saves after one second and then loads that save every few seconds. The green ball has Persist and comes back to where it was saved; the grey one has not and starts over from the scene file. In Try it saves stay in memory and are gone when the page closes.
|
||||
|
||||
| Mark | What is saved |
|
||||
|---|---|
|
||||
| `save.Persist` component | the node's transform and enabled flag |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package demo
|
||||
|
||||
import "aego/save"
|
||||
|
||||
// Checkpoint saves once, then keeps loading that save: the green ball has Persist and comes back
|
||||
// to where it was saved, the grey one has not and starts over from the scene file.
|
||||
script Checkpoint {
|
||||
@export var SaveAt float = 1
|
||||
@export var LoadEvery float = 2.5
|
||||
|
||||
var t float
|
||||
|
||||
func Update(dt float) {
|
||||
t += dt
|
||||
if !save.Exists("demo") {
|
||||
if t >= SaveAt {
|
||||
save.Save("demo")
|
||||
t = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
if t >= LoadEvery {
|
||||
save.Load("demo")
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
package demo
|
||||
|
||||
import "aego/math"
|
||||
|
||||
// Spin turns its node and bobs it up and down; edit the fields in the Inspector.
|
||||
script Spin {
|
||||
@export(unit="rad/s")
|
||||
var Speed float = 2
|
||||
@export var Bob float = 1
|
||||
@export var Rate float = 3
|
||||
|
||||
var t float
|
||||
var base Vec2
|
||||
|
||||
func Ready() {
|
||||
base = node.Transform2D().Position
|
||||
}
|
||||
|
||||
func Update(dt float) {
|
||||
t += dt
|
||||
tr := node.Transform2D()
|
||||
tr.Rotation += Speed * dt
|
||||
tr.Position = base + Vec2{0, -math.Abs(math.Sin(t * Rate)) * Bob}
|
||||
node.SetTransform2D(tr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
scene version=1 {
|
||||
node "d5500000000000000000000000000001" name="Spinner" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.Script" version=1 {
|
||||
script "game.demo.Spin" enabled=true {
|
||||
Speed=2
|
||||
Bob=1
|
||||
Rate=3
|
||||
}
|
||||
}
|
||||
node "d5500000000000000000000000000004" name="Body" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.SpriteRenderer" version=1 {
|
||||
sprite=asset("ae900000000000000000000000000010")
|
||||
layer=0
|
||||
order=0
|
||||
tint=#22c55eff
|
||||
origin=[0.5, 0.5]
|
||||
size=[2, 2]
|
||||
flipX=false
|
||||
flipY=false
|
||||
blend=premultiplied
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
}
|
||||
node "d5500000000000000000000000000002" name="Floor" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 2.6]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.SpriteRenderer" version=1 {
|
||||
sprite=asset("ae900000000000000000000000000010")
|
||||
layer=0
|
||||
order=0
|
||||
tint=#374151ff
|
||||
origin=[0.5, 0.5]
|
||||
size=[12, 0.4]
|
||||
flipX=false
|
||||
flipY=false
|
||||
blend=premultiplied
|
||||
filter=nearest
|
||||
}
|
||||
}
|
||||
node "d5500000000000000000000000000003" name="Camera" enabled=true layers=1 {
|
||||
transform {
|
||||
position=[0, 0]
|
||||
rotation=0
|
||||
scale=[1, 1]
|
||||
}
|
||||
component "engine.Camera2D" version=1 {
|
||||
zoom=40
|
||||
viewport=[0, 0, 1, 1]
|
||||
mask=-1
|
||||
order=0
|
||||
clear=true
|
||||
clearColor=#111827ff
|
||||
scale=fitVirtual
|
||||
virtual=[960, 540]
|
||||
active=true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"aego/math"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
// Spotlight pulses every enabled node with Tag; it cycles through three tags every few seconds.
|
||||
script Spotlight {
|
||||
@export var Tag string = "Enemy"
|
||||
@export var Every float = 1.5
|
||||
|
||||
var found []NodeRef
|
||||
var tags []string
|
||||
var t float
|
||||
var pick int
|
||||
|
||||
func Ready() {
|
||||
for i := 0; i < 16; i++ {
|
||||
found = append(found, nil)
|
||||
}
|
||||
tags = append(tags, Tag)
|
||||
tags = append(tags, "Enemy")
|
||||
tags = append(tags, "Pickup")
|
||||
}
|
||||
|
||||
func Update(dt float) {
|
||||
t += dt
|
||||
if t >= Every {
|
||||
t = 0
|
||||
pick = (pick + 1) % len(tags)
|
||||
}
|
||||
for k := 0; k < len(tags); k++ {
|
||||
s := 1.0
|
||||
if k == pick {
|
||||
s = 1 + 0.4 * math.Abs(math.Sin(t * 6))
|
||||
}
|
||||
n := scene.WithTag(tags[k], found)
|
||||
for i := 0; i < n; i++ {
|
||||
if k != pick && found[i].HasTag(tags[pick]) {
|
||||
continue
|
||||
}
|
||||
tr := found[i].Transform2D()
|
||||
tr.Scale = Vec2{s, s}
|
||||
found[i].SetTransform2D(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -170,7 +170,7 @@ func TestRuleCases(t *testing.T) {
|
||||
{"empty-reason", nil, []string{"[exception-syntax]", "has no reason", "unknown variant \"tset\""}},
|
||||
{"unused-exception", nil, []string{"[exception-unused] core/a -> modules/x"}},
|
||||
{"file-exception", nil, []string{"sdk/other.go: [sdk-imports-editor] imports editor/core/c"}},
|
||||
{"wasm", nil, []string{"[wasm-reaches-forbidden] reaches modules/audio via cmd/aego-playground -> modules/audio"}},
|
||||
{"wasm", nil, []string{"[wasm-reaches-forbidden] reaches modules/accessibility via cmd/aego-playground -> modules/accessibility"}},
|
||||
{"no-group", nil, []string{"weird/x: [no-group]"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -186,7 +186,7 @@ var wasmDenied = []string{
|
||||
|
||||
var wasmExtra = []string{
|
||||
"modules/render2d", "modules/scripting/...", "modules/input", "modules/physics/...", "modules/debug", "modules/tween", "modules/camera", "modules/render3d",
|
||||
"app/assembly", "app/assembly/playground", "app/playground", "cmd/aego-playground",
|
||||
"modules/spawn", "modules/savegame", "app/assembly", "app/assembly/playground", "app/playground", "cmd/aego-playground",
|
||||
}
|
||||
|
||||
// match reports whether p matches pattern: "a/b", "a/...", "..." or "*" per segment.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package main
|
||||
|
||||
import _ "aego/modules/audio"
|
||||
import _ "aego/modules/accessibility"
|
||||
|
||||
func main() {}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package accessibility
|
||||
@@ -1 +0,0 @@
|
||||
package audio
|
||||
@@ -49,8 +49,14 @@ func main() {
|
||||
if *title != "" {
|
||||
exp.Title = *title
|
||||
}
|
||||
var rel *site.Release
|
||||
if *repo != "" {
|
||||
if rel, err = site.FetchRelease(*repo); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gendocs: no download buttons:", err)
|
||||
}
|
||||
}
|
||||
rep, err := site.Build(exp, *out, site.Options{
|
||||
Playground: *play, Repo: *repo, Edit: *edit, Version: *version, Commit: *commit, Hero: *hero, Poster: *poster, Shots: *shots, Demo: *demo, PlayVersion: *playVersion, TryVersion: *tryVersion, Icons: splitList(*icons), Time: time.Now(),
|
||||
Playground: *play, Repo: *repo, Edit: *edit, Version: *version, Commit: *commit, Hero: *hero, Poster: *poster, Shots: *shots, Demo: *demo, PlayVersion: *playVersion, TryVersion: *tryVersion, Release: rel, Icons: splitList(*icons), Time: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gendocs:", err)
|
||||
|
||||
@@ -38,6 +38,14 @@ var tokenClass = [highlight.KindCount]string{
|
||||
highlight.Bracket: "br",
|
||||
}
|
||||
|
||||
// lightTokens is the code palette for the docs' light theme; the dark one is the editor's.
|
||||
var lightTokens = map[string]string{
|
||||
"kw": "#a626a4", "num": "#986801", "str": "#3f7f3a", "com": "#80838c", "op": "#4b4f58", "pun": "#6a6f7a",
|
||||
"attr": "#946200", "ns": "#b4510f", "ty": "#0b7285", "bty": "#0b7285", "fn": "#2459c7", "me": "#2459c7",
|
||||
"par": "#a3530a", "var": "#383a42", "fld": "#c0392b", "prop": "#c0392b", "con": "#986801", "en": "#986801",
|
||||
"bi": "#b0257a", "scr": "#0b7285", "err": "#d1242f", "sig": "#b4510f", "co": "#0f766e", "br": "#6a6f7a",
|
||||
}
|
||||
|
||||
func highlightAES(src string) string {
|
||||
return highlightLinked(src, nil)
|
||||
}
|
||||
@@ -110,17 +118,49 @@ func hex(c mathx.Color) string {
|
||||
return fmt.Sprintf("#%02x%02x%02x", to(c.R), to(c.G), to(c.B))
|
||||
}
|
||||
|
||||
func pageBackground() string {
|
||||
return hex(theme.Editor().Background)
|
||||
}
|
||||
|
||||
func cssColor(c mathx.Color) string {
|
||||
if c.A >= 1 {
|
||||
return hex(c)
|
||||
}
|
||||
h := hex(c)
|
||||
return fmt.Sprintf("rgb(%s %s %s / %.2f)", fmt.Sprint(hexByte(h[1:3])), fmt.Sprint(hexByte(h[3:5])), fmt.Sprint(hexByte(h[5:7])), c.A)
|
||||
}
|
||||
|
||||
func hexByte(s string) int {
|
||||
var v int
|
||||
fmt.Sscanf(s, "%02x", &v)
|
||||
return v
|
||||
}
|
||||
|
||||
func codeCSS() string {
|
||||
t := theme.Code()
|
||||
pal := highlight.Palette(&t)
|
||||
var b strings.Builder
|
||||
b.WriteString("/* generated from the editor's code theme (ui/theme.Code) */\n")
|
||||
b.WriteString(":root {\n --code-bg: " + hex(t.Background) + ";\n --code-gutter: " + hex(t.GutterBg) + ";\n --code-text: " + hex(t.Plain) + ";\n --code-line: " + hex(t.LineNo) + ";\n --code-error: " + hex(t.Error) + ";\n}\n")
|
||||
b.WriteString("/* generated from the editor's themes (ui/theme.Code and ui/theme.Editor) */\n")
|
||||
ld := func(light, dark string) string { return "light-dark(" + light + ", " + dark + ")" }
|
||||
b.WriteString(":root {\n --code-bg: " + ld("#f5f6f8", hex(t.Background)) + ";\n --code-gutter: " + ld("#eceef1", hex(t.GutterBg)) + ";\n --code-text: " + ld("#383a42", hex(t.Plain)) + ";\n --code-line: " + ld("#9aa0a9", hex(t.LineNo)) + ";\n --code-error: " + ld("#d1242f", hex(t.Error)) + ";\n")
|
||||
e := theme.Editor()
|
||||
for _, v := range []struct {
|
||||
name string
|
||||
c mathx.Color
|
||||
}{
|
||||
{"bg", e.Background}, {"header", e.Header}, {"panel", e.Panel}, {"panel-alt", e.PanelAlt}, {"hover", e.TabHot}, {"widget", e.Widget},
|
||||
{"border", e.Border}, {"separator", e.Separator}, {"text", e.Text}, {"text-dim", e.TextDim}, {"unit", e.Unit},
|
||||
{"accent", e.Accent}, {"error", e.Error}, {"warn", e.Warn}, {"ok", e.Ok},
|
||||
{"axis-x", e.AxisX}, {"axis-y", e.AxisY}, {"axis-z", e.AxisZ},
|
||||
} {
|
||||
b.WriteString(" --ed-" + v.name + ": " + cssColor(v.c) + ";\n")
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
for k, cls := range tokenClass {
|
||||
if cls == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString(".t-" + cls + " { color: " + hex(pal[k]) + "; }\n")
|
||||
b.WriteString(".t-" + cls + " { color: " + ld(lightTokens[cls], hex(pal[k])) + "; }\n")
|
||||
}
|
||||
b.WriteString(".t-com { font-style: italic; }\n")
|
||||
return b.String()
|
||||
|
||||
@@ -85,10 +85,24 @@ func (b *builder) icon(root, name, cls string) string {
|
||||
return `<svg width="16" height="16" class="` + cls + `" aria-hidden="true"><use href="` + root + `assets/icons.svg?v=` + b.assetV + `#` + name + `"/></svg>`
|
||||
}
|
||||
|
||||
// refIcons gives script packages and project settings sections the editor icon of what they cover.
|
||||
var refIcons = map[string]string{
|
||||
"aego/builtin": "cat.node", "events": "panel.events", "aego/action": "panel.tasks", "aego/audio": "cat.audio",
|
||||
"aego/camera": "cat.camera", "aego/curve": "panel.profiler", "aego/data": "file.data", "aego/debug": "panel.debug_draw",
|
||||
"aego/input": "panel.input_devices", "aego/log": "panel.console", "aego/math": "cat.transform", "aego/physics": "cat.physics",
|
||||
"aego/rand": "shape.cube", "aego/save": "panel.saves", "aego/scene": "file.scene", "aego/settings": "cat.settings",
|
||||
"aego/testing": "status.ok", "aego/time": "play.speed", "aego/timer": "file.autosave", "aego/tween": "vec.velocity",
|
||||
"section/engine": "panel.settings", "section/input": "panel.input_devices", "section/render2d": "cat.rendering",
|
||||
"section/physics": "cat.physics", "section/debug": "panel.debug_draw", "section/player": "play.start", "section/audio": "cat.audio",
|
||||
}
|
||||
|
||||
func (b *builder) entryIcon(e *Entry) string {
|
||||
if e.Icon != "" {
|
||||
return e.Icon
|
||||
}
|
||||
if ic, ok := refIcons[e.ID]; ok {
|
||||
return ic
|
||||
}
|
||||
if e.Kind == KindField && e.Field != nil {
|
||||
return fieldIcon(e.Title, e.Field.Type)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ type Entry struct {
|
||||
Related []string
|
||||
Try string
|
||||
TrySelect string
|
||||
TryScript string
|
||||
TryCode string
|
||||
TryPrefabs map[string]string
|
||||
Icon string
|
||||
EditorOnly bool
|
||||
Diag *DiagInfo
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
Tag string `json:"tag_name"`
|
||||
URL string `json:"html_url"`
|
||||
Assets []Asset `json:"assets"`
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"browser_download_url"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// download is one file offered for an OS, in the order the page prefers them.
|
||||
type download struct {
|
||||
os, label string
|
||||
match func(name string) bool
|
||||
}
|
||||
|
||||
var downloads = []download{
|
||||
{"windows", "Windows installer", func(n string) bool { return strings.HasSuffix(n, "-windows-amd64-setup.exe") }},
|
||||
{"windows", "Windows installer (.zip)", func(n string) bool { return strings.HasSuffix(n, "-windows-amd64-setup.zip") }},
|
||||
{"windows", "Windows portable (.zip)", func(n string) bool { return strings.HasSuffix(n, "-windows-amd64.zip") }},
|
||||
{"macos", "macOS (.dmg)", func(n string) bool { return strings.HasSuffix(n, ".dmg") }},
|
||||
{"linux", "Linux AppImage", func(n string) bool { return strings.HasSuffix(n, ".AppImage") }},
|
||||
{"linux", "Linux (.tar.gz)", func(n string) bool { return strings.HasSuffix(n, "-linux-amd64.tar.gz") }},
|
||||
}
|
||||
|
||||
// FetchRelease reads the latest release of a Gitea repository, such as https://git.example/owner/repo.
|
||||
func FetchRelease(repo string) (*Release, error) {
|
||||
u, err := url.Parse(strings.TrimRight(repo, "/"))
|
||||
if err != nil || u.Host == "" {
|
||||
return nil, fmt.Errorf("bad repository URL %q", repo)
|
||||
}
|
||||
api := u.Scheme + "://" + u.Host + "/api/v1/repos" + u.Path + "/releases/latest"
|
||||
c := http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := c.Get(api)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: %s", api, resp.Status)
|
||||
}
|
||||
var r Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func sizeMB(n int64) string {
|
||||
return strconv.FormatFloat(float64(n)/(1<<20), 'f', 0, 64) + " MB"
|
||||
}
|
||||
|
||||
// downloadBlock is the per-OS download button; site.js points it at the visitor's OS.
|
||||
func (b *builder) downloadBlock(root string) string {
|
||||
r := b.opt.Release
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
var items []string
|
||||
for _, d := range downloads {
|
||||
for _, a := range r.Assets {
|
||||
if d.match(a.Name) {
|
||||
items = append(items, `<li data-os="`+d.os+`"><a href="`+esc(a.URL)+`">`+esc(d.label)+`</a><span>`+sizeMB(a.Size)+"</span></li>")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
fallback := r.URL
|
||||
if u, ok := b.urls[installPage]; ok {
|
||||
fallback = root + u
|
||||
}
|
||||
return `<div class="download" data-fallback="` + esc(fallback) + `"><div class="dl-head"><a class="button primary dl-main" href="` + esc(r.URL) + `">` + b.icon(root, "file.save", "") + `<span>Download</span></a>` +
|
||||
`<span class="dl-meta">` + esc(r.Tag) + ` · <a href="` + esc(r.URL) + `">all files</a></span></div><ul class="dl-list">` + strings.Join(items, "") + "</ul></div>\n"
|
||||
}
|
||||
+206
-75
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@ func crawl(t *testing.T, dir string) map[string]string {
|
||||
file = path.Join(file, "index.html")
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(file, ".css") || strings.HasSuffix(file, ".js") {
|
||||
if strings.HasSuffix(file, ".css") || strings.HasSuffix(file, ".js") || strings.HasSuffix(file, ".woff2") {
|
||||
if _, err := os.Stat(filepath.Join(dir, file)); err != nil {
|
||||
t.Errorf("%s: missing asset %s", p, href)
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func TestShell(t *testing.T) {
|
||||
`Aego 1.2.3`,
|
||||
`<a href="#install">Install</a>`,
|
||||
`class="next" href="language.html"`,
|
||||
`data-group="start" open data-current="true"`,
|
||||
`class="item head current" aria-current="page"`,
|
||||
`aria-current="page"`,
|
||||
} {
|
||||
if !strings.Contains(w, want) {
|
||||
@@ -211,7 +211,7 @@ func TestShell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
co := pages["language/coroutines.html"]
|
||||
if !strings.Contains(co, `<nav class="crumbs"><span>Guides</span><span class="sep">/</span><span>Aego Script</span><span class="sep">/</span><a href="../language.html">Aego Script</a></nav>`) {
|
||||
if !strings.Contains(co, `<nav class="crumbs"><span>Guides</span><span class="sep">/</span><a href="../language.html">Aego Script</a></nav>`) {
|
||||
t.Error("breadcrumbs are wrong on the coroutines page")
|
||||
}
|
||||
if !strings.Contains(pages["index.html"], `class="button primary" href="welcome.html"`) {
|
||||
@@ -335,3 +335,37 @@ func TestTryBlock(t *testing.T) {
|
||||
t.Error("a try block without the docs runtime")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadBlock(t *testing.T) {
|
||||
rel := &Release{Tag: "v1.0", URL: "https://git.example/aego/releases/tag/v1.0", Assets: []Asset{
|
||||
{Name: "aego-1.0-linux-amd64.tar.gz", URL: "https://git.example/l.tar.gz", Size: 12 << 20},
|
||||
{Name: "aego-1.0-windows-amd64-setup.zip", URL: "https://git.example/w.zip", Size: 17 << 20},
|
||||
{Name: "SHA256SUMS.txt", URL: "https://git.example/sums", Size: 300},
|
||||
}}
|
||||
_, _, pages := build(t, fixture(), Options{Release: rel})
|
||||
home := pages["index.html"]
|
||||
for _, want := range []string{`class="button primary dl-main"`, `<li data-os="linux"><a href="https://git.example/l.tar.gz">Linux (.tar.gz)</a><span>12 MB</span></li>`, `data-os="windows"`, `v1.0`} {
|
||||
if !strings.Contains(home, want) {
|
||||
t.Errorf("home page download block has no %s", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(home, "SHA256SUMS") || strings.Contains(home, `data-os="macos"`) {
|
||||
t.Error("download block lists a file it should not")
|
||||
}
|
||||
if strings.Contains(home, `class="button primary" href="welcome.html"`) {
|
||||
t.Error("two primary buttons on the landing page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaTags(t *testing.T) {
|
||||
_, _, pages := build(t, fixture(), Options{})
|
||||
if !strings.Contains(pages["index.html"], `<meta name="description" content="A 2D-first game engine`) {
|
||||
t.Error("landing page has no description")
|
||||
}
|
||||
if !strings.Contains(pages["language.html"], `<meta property="og:description" content="The language.">`) {
|
||||
t.Error("a page's description is not its summary")
|
||||
}
|
||||
if got := metaDescription(" See [the guide](g.html) and `code`. " + strings.Repeat("word ", 50)); strings.ContainsAny(got, "[]`!") || len(got) > 165 {
|
||||
t.Errorf("metaDescription = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user