122 Commits

Author SHA1 Message Date
John Alanbrook
c014f29ce7 fill out box2d methods
Some checks failed
Build and Deploy / build-macos (push) Failing after 6s
Build and Deploy / build-linux (push) Failing after 1m30s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-05-25 01:58:10 -05:00
John Alanbrook
f1b3e5eddc Merge branch 'master' into box2d
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-24 22:45:52 -05:00
John Alanbrook
792da2ce4b Merge remote-tracking branch 'origin/misty'
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-24 22:24:01 -05:00
John Alanbrook
0c9d78a3d3 initialize parts of SDL only when required, and return errors
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-24 21:30:29 -05:00
John Alanbrook
e929f43a96 add steam module 2025-05-24 21:29:58 -05:00
John Alanbrook
01eff40690 auto latest docker image
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Waiting to run
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-linux (push) Failing after 2m15s
2025-05-24 09:37:29 -05:00
John Alanbrook
23813a4c31 http downloading in chunks 2025-05-24 00:55:56 -05:00
John Alanbrook
1248b94244 remove curl and openssl dependencies
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Successful in 1m44s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-05-24 00:07:37 -05:00
John Alanbrook
f754d91e14 fix compilation errors on windows and linux
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Failing after 1m59s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-05-23 18:24:52 -05:00
John Alanbrook
7246016b8b example nat 2025-05-23 14:23:52 -05:00
John Alanbrook
b42eec96f6 separate the idea of misty actor and scene tree actor
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Failing after 1m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-05-23 12:20:47 -05:00
John Alanbrook
efd98460c5 -u is no longer available as a global, only within the running actor code; enable passing args to use
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-22 18:59:57 -05:00
John Alanbrook
698dbd81ae removed unneeded functions and split out actor specific functions 2025-05-22 18:16:51 -05:00
John Alanbrook
13c2a0ba0c sprites, rtrees, and transforms made with constructor functions 2025-05-22 17:00:31 -05:00
John Alanbrook
d0fdb469dd rtrees are now created as a constructor 2025-05-22 16:30:48 -05:00
John Alanbrook
32366483dc split out debug 2025-05-22 16:17:11 -05:00
John Alanbrook
707b2845b1 split out spline and js 2025-05-22 16:05:42 -05:00
John Alanbrook
693087afae move rtree to its own module 2025-05-22 15:42:45 -05:00
John Alanbrook
51940080a8 fully compiles 2025-05-22 13:23:13 -05:00
John Alanbrook
a204fce4b5 initial refactor
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-macos (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-05-22 11:48:27 -05:00
John Alanbrook
7bab2f1b7a clean up graphics
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m32s
Build and Deploy / build-macos (push) Failing after 7s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-05-22 01:28:03 -05:00
John Alanbrook
f5ee3aada6 fix portal connection
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-linux (push) Failing after 1m34s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-05-21 23:56:02 -05:00
John Alanbrook
449e25e0f3 fix non local host networking
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-macos (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-21 13:02:30 -05:00
John Alanbrook
3cbb95831c networked chess example
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-21 12:25:17 -05:00
John Alanbrook
146baf1d23 networked chess 2025-05-21 10:34:33 -05:00
John Alanbrook
e7cc716590 initial moth
Some checks failed
Build and Deploy / build-macos (push) Failing after 10s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-19 09:01:17 -05:00
John Alanbrook
3aa2d549d1 add claude.md 2025-05-18 22:24:31 -05:00
John Alanbrook
901012064a add chess example
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-macos (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-18 08:57:34 -05:00
John Alanbrook
bf2336a172 add cwd command line arg 2025-05-18 08:56:25 -05:00
John Alanbrook
708a112449 add AGENTS.md and fix rect render
Some checks failed
Build and Deploy / build-macos (push) Failing after 9s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-17 23:06:42 -05:00
John Alanbrook
85ee724754 add texture mode for renderer
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-11 09:34:31 -05:00
John Alanbrook
ff2ee3d6db add animation test; help qr API
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-09 12:57:54 -05:00
John Alanbrook
6bc04830d3 load image/texture from arraybuffer
Some checks failed
Build and Deploy / build-macos (push) Failing after 3s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-07 08:13:22 -05:00
John Alanbrook
589bb365bd http now returns byte array and content type
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-06 22:50:24 -05:00
John Alanbrook
0b8a43eb91 Merge branch 'https' into misty
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-macos (push) Failing after 3s
Build and Deploy / build-linux (push) Failing after 1m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-05-06 19:07:32 -05:00
John Alanbrook
bb3087dc37 fix qr code encoding to always be array buffers instead of strings 2025-05-06 18:53:08 -05:00
John Alanbrook
92f56570d9 fix qr decoding 2025-05-06 12:48:13 -05:00
John Alanbrook
938da0d4dc Use thin lto for release builds; move 'strip' to meson.build for release
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Successful in 1m44s
Build and Deploy / build-macos (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-05-05 13:55:09 -05:00
John Alanbrook
3d94859151 macos build
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-macos (push) Failing after 1m46s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-05-04 17:50:03 -05:00
John Alanbrook
a85b1873dd sprite rework
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-04 11:28:45 -05:00
John Alanbrook
446ad080e1 render sprite geometry 2025-05-02 11:18:13 -05:00
John Alanbrook
ead61e648a changes to update to newer quickjs version
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-01 12:50:03 -05:00
John Alanbrook
600fbfd3b7 fast sprite render
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-05-01 01:35:05 -05:00
John Alanbrook
f3031d6cd0 add bcrypt as windows dependency
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-30 14:09:11 -05:00
John Alanbrook
7152ae093e audio working from soloud -> sdl3
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-29 12:43:25 -05:00
John Alanbrook
2bd93ff9e0 closer to web build 2025-04-29 12:43:17 -05:00
John Alanbrook
f68e45f898 sdl audio
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-28 08:48:44 -05:00
John Alanbrook
7eca07c7d1 fix actor delay time always being 0 2025-04-26 09:42:23 -05:00
John Alanbrook
ee4ec2fc39 fix draw.image
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-26 08:08:15 -05:00
John Alanbrook
b93a5a3ac0 refactor draw2d and render
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-25 17:56:17 -05:00
John Alanbrook
de63e0e52d round rect and rounded filled rect
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-24 14:04:27 -05:00
John Alanbrook
daef2fd2f2 circle and elipse
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-24 09:19:56 -05:00
John Alanbrook
6705ce8980 sdl renderer
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-23 20:39:07 -05:00
John Alanbrook
f443816355 render
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-22 11:18:21 -05:00
John Alanbrook
c8c08d5fbe renderer now has camera matrix
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-21 09:23:50 -05:00
John Alanbrook
b8328657df add camera test
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-04-14 22:46:07 -05:00
John Alanbrook
8d235ddf12 add cycle detection with use 2025-04-14 18:01:41 -05:00
John Alanbrook
05f284e3fa remove unnecessary functions
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-14 09:05:49 -05:00
John Alanbrook
566baa250c sdl renderer
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
'
2025-04-13 21:32:34 -05:00
John Alanbrook
19a8bd41a9 update sdl3
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-04-03 13:12:57 -05:00
John Alanbrook
58cad839b6 fix crash
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m15s
Build and Deploy / build-windows (CLANG64) (push) Failing after 15m44s
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-03-28 14:44:57 -05:00
John Alanbrook
34035ae6ac sdl renderer backend
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-03-28 12:29:15 -05:00
John Alanbrook
3a4547fb80 add blob; pull out crypto, time; add sdl_renderer
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-03-27 14:31:02 -05:00
John Alanbrook
86b21bb6dd inital parseq add
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-03-26 09:42:50 -05:00
John Alanbrook
8cf114cbb4 io actor
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-03-23 04:46:16 -05:00
John Alanbrook
f9100da8a2 -u.unneeded 2025-03-22 22:27:36 -05:00
John Alanbrook
f9c1a3e71a root actor runs only on main thread; restrict ffi main thread functions to it 2025-03-22 20:55:20 -05:00
John Alanbrook
73594c8599 fix memory leaking and thread sync problems 2025-03-22 18:24:18 -05:00
John Alanbrook
239f35389e fix crashing assert on free
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-03-22 11:15:40 -05:00
John Alanbrook
95d3296dd9 wota now encodes at the C level; update dmon for macos 13; clean up many warnings
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m15s
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-03-20 17:25:48 -05:00
John Alanbrook
c566f90d16 remove script
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-03-19 17:52:44 -05:00
John Alanbrook
9410af3a69 messages
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m11s
Build and Deploy / build-windows (CLANG64) (push) Failing after 6m17s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-19 12:40:31 -05:00
John Alanbrook
8627fc52ef actors in C now
Some checks failed
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Successful in 1m15s
Build and Deploy / build-windows (CLANG64) (push) Failing after 11m46s
2025-03-18 18:18:56 -05:00
John Alanbrook
813cc8dbbc make tracy work for multilpe contexts
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-03-15 20:49:53 -05:00
John Alanbrook
d90d81d7ff fixed memory leak on thread exit
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m41s
Build and Deploy / build-windows (CLANG64) (push) Failing after 6m16s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-15 08:33:37 -05:00
John Alanbrook
b1f62cc58c destroy mailboxes on thread exit
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m42s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m57s
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-03-13 17:29:42 -05:00
John Alanbrook
38da997069 use wota for on machine message passing 2025-03-13 17:20:04 -05:00
John Alanbrook
88d5f6455b portal spawning works 2025-03-13 15:21:11 -05:00
John Alanbrook
eb3a41be69 add wota replacer and reviver 2025-03-13 15:21:01 -05:00
John Alanbrook
1332af93ab enet and mailboxes now take strings or array buffers
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m11s
Build and Deploy / build-windows (CLANG64) (push) Failing after 6m3s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-13 06:28:00 -05:00
John Alanbrook
93adf50498 add nota replacer and reviver options; don't serialize functions 2025-03-13 06:25:15 -05:00
John Alanbrook
291fd9ead0 hide actor data
Some checks failed
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Successful in 1m16s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m57s
2025-03-12 23:09:43 -05:00
John Alanbrook
e86138ec00 multirheading mailboxes fixed
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m20s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m58s
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
2025-03-11 20:34:39 -05:00
John Alanbrook
c431f117e9 remove atoms for multithreading
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m42s
Build and Deploy / build-windows (CLANG64) (push) Failing after 6m13s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-11 10:13:15 -05:00
John Alanbrook
847a3ef314 threading
Some checks failed
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Failing after 55s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m50s
2025-03-10 22:49:48 -05:00
John Alanbrook
bab09fed6d add documentation
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m22s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-09 21:53:25 -05:00
John Alanbrook
d56c983e01 add return to callback send 2025-03-09 10:18:43 -05:00
John Alanbrook
69df7302d5 initial attempt at portal and contact
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m41s
Build and Deploy / build-windows (CLANG64) (push) Failing after 9m19s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-06 21:18:05 -06:00
John Alanbrook
01f7e715a4 parent child handshake
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m45s
Build and Deploy / build-windows (CLANG64) (push) Failing after 8m5s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-06 11:31:37 -06:00
John Alanbrook
e71a823848 add actor clock, random, unneeded
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m45s
Build and Deploy / build-windows (CLANG64) (push) Failing after 9m53s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-05 22:34:08 -06:00
John Alanbrook
a8865594ca coupling
Some checks failed
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Successful in 1m13s
Build and Deploy / build-windows (CLANG64) (push) Failing after 14m3s
2025-03-05 13:03:44 -06:00
John Alanbrook
23d764c534 actor detection
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m42s
Build and Deploy / build-windows (CLANG64) (push) Failing after 8m47s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-05 09:01:29 -06:00
John Alanbrook
c7aee73dcb initial misty implementation
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m41s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m50s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-04 22:46:10 -06:00
John Alanbrook
925d1fc437 fix various graphics and sound issues
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m11s
Build and Deploy / build-windows (CLANG64) (push) Failing after 9m56s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-04 22:43:19 -06:00
John Alanbrook
a6dbedb3cd add nota benchmark; builds no longer continue on test fail 2025-03-04 22:41:51 -06:00
John Alanbrook
74a6b9bfe6 Add versioning dropdown to docs 2025-03-04 22:41:51 -06:00
John Alanbrook
40126060fb dramatically improve nota speed for nested arrays and objects
Some checks are pending
Build and Deploy / build-windows (CLANG64) (push) Waiting to run
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Successful in 1m19s
2025-03-03 18:35:46 -06:00
John Alanbrook
6032c034bc add wota 2025-03-03 18:35:28 -06:00
John Alanbrook
6b4062eee6 add http get
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m3s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m45s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-03 08:07:16 -06:00
John Alanbrook
0ea21e86eb add qr code encode and decode
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m15s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m2s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-03-03 08:06:18 -06:00
John Alanbrook
30c5da879b fix various graphics and sound issues 2025-02-27 16:51:48 -06:00
John Alanbrook
1a76081fec add nota benchmark; builds no longer continue on test fail 2025-02-26 11:16:35 -06:00
John Alanbrook
2594c03765 initial box2d integration
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m12s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m33s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-26 07:47:04 -06:00
John Alanbrook
045c4b49ef Add versioning dropdown to docs 2025-02-25 10:14:17 -06:00
John Alanbrook
af0996f6ab nota write decimal numbers via a string in the qjs_nota implementation, solving windows fp error
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m11s
Build and Deploy / build-windows (CLANG64) (push) Successful in 10m2s
Build and Deploy / package-dist (push) Successful in 8s
Build and Deploy / deploy-gitea (push) Successful in 5s
Build and Deploy / deploy-itch (push) Successful in 9s
2025-02-24 23:15:58 -06:00
John Alanbrook
6c390aeae3 add documentation for dmon, enet, and nota.
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m11s
Build and Deploy / build-windows (CLANG64) (push) Successful in 13m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-24 19:04:16 -06:00
John Alanbrook
e9519484cc fix enet; add enet testing
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-02-24 13:43:01 -06:00
John Alanbrook
b7da920f31 add qjs_soloud to source tree 2025-02-24 13:37:02 -06:00
John Alanbrook
35647a5c5b Minor nota speed improvement; use nota growable array internally so no more fixed size
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m15s
Build and Deploy / build-windows (CLANG64) (push) Successful in 14m57s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-24 11:25:12 -06:00
John Alanbrook
8ea8f7fec7 Add qjs_enet into prosperon source 2025-02-24 11:15:22 -06:00
John Alanbrook
5254b84704 fixed nota encoding/decoding bug with arrays longer than 126 elements 2025-02-23 17:16:00 -06:00
John Alanbrook
f728a217c9 add dmon doc; dmon now dispatches via prosperon.on 2025-02-23 17:15:35 -06:00
John Alanbrook
c27817b73a dmon now only watches top level directory; user must call dmon.watch and supply a dmon watch function
Some checks failed
Build and Deploy / build-linux (push) Successful in 1m7s
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-02-23 16:07:09 -06:00
John Alanbrook
7ea79c8ced Fix nota implementation; add nota test suite 2025-02-23 16:06:40 -06:00
John Alanbrook
fb10c63882 add dmon and nota into source tree, out of subprojects; build on macos
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m9s
Build and Deploy / build-windows (CLANG64) (push) Successful in 33m33s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-23 08:49:49 -06:00
John Alanbrook
96ef8ccba3 Add camera and debug modules
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
2025-02-21 17:15:58 -06:00
John Alanbrook
6148f18340 CLI version ends with a newline 2025-02-21 16:55:45 -06:00
John Alanbrook
867a18e788 Project cleanup. Update stb libraries. Remove unused files. Enable pedantic warning flag and fix all warnings. Fill out spline file. 2025-02-21 16:38:59 -06:00
John Alanbrook
387c4364b5 SDL3 built as a static library as part of build process
All checks were successful
Build and Deploy / build-linux (push) Successful in 1m12s
Build and Deploy / build-windows (CLANG64) (push) Successful in 14m52s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-21 11:48:28 -06:00
John Alanbrook
60dce4a08f imgui is always compiled in, and developer selects to enable or disable its drawing; fix bug with rendering lines that caused prosperon to crash
All checks were successful
Build and Deploy / build-linux (push) Successful in 34s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m15s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-20 17:28:27 -06:00
John Alanbrook
d2325c20bd Add MSYS2 CI
All checks were successful
Build and Deploy / build-linux (push) Successful in 36s
Build and Deploy / build-windows (CLANG64) (push) Successful in 6m12s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-20 14:31:28 -06:00
John Alanbrook
29295607df Update build action for more useful distribution
All checks were successful
Build and Deploy / build-linux (push) Successful in 41s
Build and Deploy / build-windows (push) Successful in 47s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
2025-02-19 16:50:25 -06:00
240 changed files with 42423 additions and 10930 deletions

View File

@@ -1,13 +1,15 @@
name: Build name: Build and Deploy
on: on:
push: push:
branches: [ "*" ] branches: [ "*" ]
tags: [ "v*" ]
pull_request: pull_request:
release:
types: [published]
jobs: jobs:
# ──────────────────────────────────────────────────────────────
# LINUX BUILD
# ──────────────────────────────────────────────────────────────
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
@@ -15,44 +17,16 @@ jobs:
steps: steps:
- name: Check Out Code - name: Check Out Code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Cache SDL3 (Linux)
uses: actions/cache@v3
with:
path: |
sdl3
sdl3-build
key: sdl3-linux-${{ hashFiles('sdl3/CMakeLists.txt') }}
- name: Build SDL3 (Linux)
run: |
if [ ! -d "sdl3/.git" ]; then
echo "Cloning SDL3 repository..."
git clone --depth 1 --branch main https://github.com/libsdl-org/SDL.git sdl3
else
echo "SDL3 source is already present (possibly from cache)."
fi
mkdir -p sdl3-build
cd sdl3-build
cmake ../sdl3 -GNinja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/installed_sdl3" \
-DSDL_SHARED=ON \
-DSDL_STATIC=OFF
ninja
ninja install
- name: Build Prosperon (Linux) - name: Build Prosperon (Linux)
run: | run: |
export PKG_CONFIG_PATH="${PWD}/sdl3-build/installed_sdl3/lib/pkgconfig:$PKG_CONFIG_PATH" meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true
meson compile -C build meson compile -C build
- name: Test Prosperon (Linux) - name: Test Prosperon (Linux)
env: env: { TRACY_NO_INVARIANT_CHECK: 1 }
TRACY_NO_INVARIANT_CHECK: 1
run: | run: |
meson test --print-errorlogs -C build meson test --print-errorlogs -C build
@@ -63,102 +37,146 @@ jobs:
name: testlog-linux name: testlog-linux
path: build/meson-logs/testlog.txt path: build/meson-logs/testlog.txt
- name: Create artifact folder (Linux)
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
run: |
mkdir _pack
cp build/prosperon _pack/
cp sdl3-build/installed_sdl3/lib/libSDL3.so _pack/
- name: Upload Artifact (Linux) - name: Upload Artifact (Linux)
if: ${{ github.event_name == 'release' && github.event.action == 'published' }} if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: prosperon-artifacts-linux name: prosperon-artifacts-linux
path: _pack path: build/prosperon
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: gitea.pockle.world
username: ${{ secrets.USER_GITEA }}
password: ${{ secrets.TOKEN_GITEA }}
- name: Determine Docker Tag
id: docker_tag
run: |
if [[ "${{ github.ref }}" =~ ^refs/tags/v.* ]]; then
TAG=$(echo "${{ github.ref }}" | sed 's#refs/tags/##')
echo "tag=$TAG" >> $GITHUB_OUTPUT
else
echo "tag=latest" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker Image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: gitea.pockle.world/john/prosperon:${{ steps.docker_tag.outputs.tag }}
platforms: linux/amd64
# ──────────────────────────────────────────────────────────────
# WINDOWS BUILD (MSYS2 / CLANG64)
# ──────────────────────────────────────────────────────────────
build-windows: build-windows:
runs-on: ubuntu-latest runs-on: win-native
container: strategy:
image: gitea.pockle.world/john/prosperon/linux:latest matrix: { msystem: [ CLANG64 ] }
steps: steps:
- name: Check Out Code - name: Check Out Code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Cache SDL3 (Windows cross) - name: Setup MSYS2
uses: actions/cache@v3 uses: msys2/setup-msys2@v2
with: with:
path: | msystem: ${{ matrix.msystem }}
sdl3-win update: true
sdl3-build-win cache: true
key: sdl3-win-${{ hashFiles('sdl3-win/CMakeLists.txt') }} install: |
git zip gzip tar base-devel
pacboy: |
meson
cmake
toolchain
- name: Build SDL3 (Windows cross) - name: Build Prosperon (Windows)
shell: msys2 {0}
run: | run: |
if [ ! -d "sdl3-win/.git" ]; then meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
echo "Cloning SDL3 for Windows cross..."
git clone --depth 1 --branch main https://github.com/libsdl-org/SDL.git sdl3-win
else
echo "SDL3-win source already present (possibly from cache)."
fi
mkdir -p sdl3-build-win
cd sdl3-build-win
cmake ../sdl3-win -GNinja \
-DCMAKE_SYSTEM_NAME=Windows \
-DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \
-DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \
-DCMAKE_RC_COMPILER=x86_64-w64-mingw32-windres \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/installed_sdl3_win" \
-DSDL_SHARED=ON \
-DSDL_STATIC=OFF
ninja
ninja install
- name: Configure PKG_CONFIG_PATH (Windows cross)
run: |
echo "PKG_CONFIG_PATH=${GITHUB_WORKSPACE}/sdl3-build-win/installed_sdl3_win/lib/pkgconfig:$PKG_CONFIG_PATH" >> $GITHUB_ENV
- name: Build Prosperon (Windows cross)
run: |
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true --cross-file mingw32.cross
meson compile -C build meson compile -C build
- name: Test Prosperon - name: Test Prosperon (Windows)
shell: msys2 {0}
env: env:
TRACY_NO_INVARIANT_CHECK: 1 TRACY_NO_INVARIANT_CHECK: 1
run: | run: |
meson test --print-errorlogs -C build meson test --print-errorlogs -C build
- name: Upload Test Log - name: Upload Test Log (Windows)
if: ${{ always() }} if: ${{ always() }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: testlog-linux name: testlog-windows
path: build/meson-logs/testlog.txt path: build/meson-logs/testlog.txt
- name: Create package folder - name: Upload Artifact (Windows)
run: | if: startsWith(github.ref, 'refs/tags/v')
mkdir _pack
cp build/prosperon.exe _pack/
cp sdl3-build-win/installed_sdl3_win/bin/SDL3.dll _pack/
- name: Upload Artifact (Windows cross)
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: prosperon-artifacts-windows name: prosperon-artifacts-windows
path: _pack path: build/prosperon.exe
# ──────────────────────────────────────────────────────────────
# MACOS BUILD
# ──────────────────────────────────────────────────────────────
build-macos:
runs-on: macos-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Build Prosperon (macOS)
run: |
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
meson compile -C build
- name: Test Prosperon (macOS)
run: |
meson test --print-errorlogs -C build
- name: Upload Test Log (macOS)
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: testlog-macos
path: build/meson-logs/testlog.txt
- name: Upload Artifact (macOS)
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v3
with:
name: prosperon-artifacts-macos
path: build/prosperon
# ──────────────────────────────────────────────────────────────
# PACKAGE CROSS-PLATFORM DIST
# ──────────────────────────────────────────────────────────────
package-dist: package-dist:
if: ${{ github.event_name == 'release' && github.event.action == 'published' }} needs: [ build-linux, build-windows, build-macos ]
needs: [build-linux, build-windows] if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check Out Code - name: Check Out Code
uses: actions/checkout@v3 uses: actions/checkout@v3
with: { fetch-depth: 0 }
- name: Get Latest Tag
id: get_tag
run: |
TAG=$(git describe --tags --abbrev=0)
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Download Linux Artifacts - name: Download Linux Artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@@ -172,24 +190,115 @@ jobs:
name: prosperon-artifacts-windows name: prosperon-artifacts-windows
path: windows_artifacts path: windows_artifacts
- name: Create the Dist Folder - name: Download macOS Artifacts
uses: actions/download-artifact@v3
with:
name: prosperon-artifacts-macos
path: mac_artifacts
- name: Create Dist Folder
run: | run: |
mkdir dist mkdir -p dist/linux dist/win dist/mac
cp README.md dist/
cp README.md dist/ cp license.txt dist/
cp license.txt dist/ cp -r examples dist/
cp -r examples dist/ cp linux_artifacts/* dist/linux/
# Make subdirectories for each platform
mkdir dist/linux
mkdir dist/win
# Copy artifacts in
cp linux_artifacts/* dist/linux/
cp windows_artifacts/* dist/win/ cp windows_artifacts/* dist/win/
cp mac_artifacts/* dist/mac/
- name: Package Final Dist
run: |
TAG=${{ steps.get_tag.outputs.tag }}
zip -r "prosperon-${TAG}.zip" dist
echo "Created prosperon-${TAG}.zip"
- name: Upload Final Dist - name: Upload Final Dist
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: prosperon name: "prosperon-${{ steps.get_tag.outputs.tag }}"
path: "prosperon-${{ steps.get_tag.outputs.tag }}.zip"
# ──────────────────────────────────────────────────────────────
# DEPLOY TO ITCH.IO (single ZIP containing all OSes)
# ──────────────────────────────────────────────────────────────
deploy-itch:
needs: [ package-dist ]
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v3
with: { fetch-depth: 0 }
- name: Get Latest Tag
id: get_tag
run: |
TAG=$(git describe --tags --abbrev=0)
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Download Final Distribution
uses: actions/download-artifact@v3
with:
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
path: dist path: dist
- name: Set up Butler
uses: jdno/setup-butler@v1
- name: Push to itch.io
run: |
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" \
${{ secrets.ITCHIO_USERNAME }}/prosperon:universal \
--userversion ${{ steps.get_tag.outputs.tag }}
env:
BUTLER_API_KEY: ${{ secrets.ITCHIO_API_KEY }}
# ──────────────────────────────────────────────────────────────
# DEPLOY TO SELF-HOSTED GITEA
# ──────────────────────────────────────────────────────────────
deploy-gitea:
needs: [ package-dist ]
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v3
with: { fetch-depth: 0 }
- name: Get Latest Tag & Commit Message
id: get_tag
run: |
TAG=$(git describe --tags --abbrev=0)
COMMIT_MSG=$(git log -1 --pretty=%B "$TAG")
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
- name: Download Final Distribution
uses: actions/download-artifact@v3
with:
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
path: dist
- name: Create / Update Gitea Release
run: |
TAG=${{ steps.get_tag.outputs.tag }}
ZIP=dist/prosperon-${TAG}.zip
BODY=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.')
RELEASE=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/$TAG" | jq -r '.id')
if [ "$RELEASE" = "null" ] || [ -z "$RELEASE" ]; then
RELEASE=$(curl -X POST \
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"$TAG\",\"body\":$BODY,\"draft\":false,\"prerelease\":false}" \
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases" | jq -r '.id')
fi
curl -X POST \
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$ZIP" \
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/$RELEASE/assets?name=prosperon-${TAG}.zip"
env:
TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}

View File

@@ -1,64 +0,0 @@
name: Build
jobs:
# ===============================================================
# MACOS BUILD (Using Homebrew SDL3)
# ===============================================================
build-macos:
runs-on: macos-latest
continue-on-error: true
steps:
# 1) Check out code
- name: Check Out Code
uses: actions/checkout@v3
# 2) Install dependencies (SDL3 via Homebrew) + ccache
- name: Install Dependencies (macOS)
run: |
brew update
brew install sdl3 meson ninja cmake ccache
# 3) Configure ccache
- name: Configure ccache
run: |
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
# 4) Cache ccache
- name: Cache ccache
uses: actions/cache@v3
with:
path: ~/Library/Caches/ccache
key: ccache-macos-${{ hashFiles('**/*.c', '**/*.cpp', '**/*.h', '**/CMakeLists.txt', '**/meson.build') }}
restore-keys: |
ccache-macos-
# 5) Build Prosperon (macOS) linking against Homebrew's SDL3
- name: Build Prosperon (macOS)
run: |
# Ensure pkg-config can find Homebrew's SDL3 .pc files
export PKG_CONFIG_PATH="$(brew --prefix sdl3)/lib/pkgconfig:$PKG_CONFIG_PATH"
meson setup build_macos -Dbuildtype=release -Db_lto=true -Db_ndebug=true
meson compile -C build_macos
# 6) Copy SDL3 .dylib from Homebrew for packaging
- name: Copy SDL3 library for packaging
run: |
SDL3_PREFIX=$(brew --prefix sdl3)
mkdir -p sdl3-macos
# Copy all versions of the SDL3 dynamic library
cp -a "${SDL3_PREFIX}/lib/libSDL3*.dylib" sdl3-macos/ || echo "No .dylib found, ignoring"
# 7) Create minimal artifact folder (macOS)
- name: Create artifact folder (macOS)
run: |
mkdir _pack
cp build_macos/prosperon _pack/
cp sdl3-macos/libSDL3*.dylib _pack/ || echo "No .dylib found, ignoring"
# 8) Upload artifact (macOS)
- name: Upload Artifact (macOS)
uses: actions/upload-artifact@v3
with:
name: prosperon-artifacts-macos
path: _pack

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ game.zip
icon.ico icon.ico
steam/ steam/
subprojects/*/ subprojects/*/
build_dbg/

27
AGENTS.md Normal file
View File

@@ -0,0 +1,27 @@
# AGENTS.md
## Project Overview
This is a game engine developed using a QuickJS fork as its scripting language. It is an actor based system, based on Douglas Crockford's Misty. It is a Meson compiled project with a number of dependencies.
## File Structure
- `source/`: Contains the C source code
- `scripts/`: Contains script code that is loaded on executable start, and modules
- `shaders/`: Contains shaders that ship with the engine (for shader based backends)
- `benchmarks/`: Benchmark programs for testing speed
- `tests/`: Unit tests
- `examples/`: Contains full game examples
## Coding Practices
- Use K&R style C
- Use as little whitespace as possible
- Javascript style prefers objects and prototypical inheritence over ES6 classes, liberal use of closures, and var everywhere
## Instructions
- When generating code, adhere to the coding practices outlined above.
- When adding new features, ensure they align with the project's goals.
- When fixing bugs, review the code carefully before making changes.
- When writing unit tests, cover all important scenarios.
## Compiling, running, and testing
- To compile the code, run "make", which generates a prosperon executable in build_dbg/, and copy it into the root folder
- Run a test by giving it as its command: so ./prosperon tests/overling.js would run the test overling.js, ./prosperon tests/nota.js runs the nota benchmark

400
CLAUDE.md Normal file
View File

@@ -0,0 +1,400 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
### Build variants
- `make debug` - Build debug version (uses meson debug configuration)
- `make fast` - Build optimized version
- `make release` - Build release version with LTO and optimizations
- `make small` - Build minimal size version
- `make web` - Build for web/emscripten platform
- `make crosswin` - Cross-compile for Windows using mingw32
### Testing
- `meson test -C build_dbg` - Run all tests in debug build
- `meson test -C build_<variant>` - Run tests in specific build variant
- `./build_dbg/prosperon tests/<testname>.js` - Run specific test
- Available tests: `spawn_actor`, `empty`, `nota`, `wota`, `portalspawner`, `overling`, `send`, `delay`
### Common development commands
- `meson setup build_<variant>` - Configure build directory
- `meson compile -C build_<variant>` - Compile in build directory
- `./build_dbg/prosperon examples/<example>` - Run example from build directory
- Copy prosperon to game directory and run: `cp build_dbg/prosperon <game-dir>/ && cd <game-dir> && ./prosperon`
## Architecture Overview
Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty system. Key architectural principles:
### Actor Model
- Each actor runs on its own thread
- Communication only through message passing (no shared JavaScript objects)
- Hierarchical actor system with spawning/killing
- Actor lifecycle: awake, update, draw, garbage collection
### JavaScript Style Guide
- Use `use()` function for imports (Misty-style, not ES6 import/export)
- Prefer closures and javascript objects and prototypes over ES6 style classes
- Follow existing JavaScript patterns in the codebase
- Functions as first-class citizens
- Do not use const or let; only var
### Core Systems
1. **Actor System** (scripts/core/engine.js)
- Message passing via `send()`, `$_.receive()`
- Actor spawning/management
- Register-based component system (update, draw, gui, etc.)
2. **Module System**
- `use()` function for loading modules
- Module paths: `scripts/modules/`, `scripts/modules/ext/`
- Custom QuickJS build with embedded C modules
3. **Build System**
- Meson build configuration (Makefile is convenience wrapper)
- Multiple platform targets (Windows, macOS, Linux, Web)
- Custom QuickJS build in `subprojects/`
- Uses SDL3 for cross-platform support
### Engine Entry Points
- `source/prosperon.c` - Main C entry point
- `scripts/core/engine.js` - JavaScript engine initialization for system
- `scripts/core/base.js` has modifications to this Javascript runtime (for example, additions to the base Array, String, etc)
### Subprojects
- C code has many subprojects, who's source and sometimes documentation can be found in subprojects. subprojects/quickjs/doc has documentation for quickjs
### Resource System
- Scripts are bundled into `core.zip` during build
- Runtime module loading via PhysFS
- Resource paths checked in order: `/`, `scripts/modules/`, `scripts/modules/ext/`
### Notable Dependencies
- QuickJS (custom build) - JavaScript runtime
- SDL3 - Platform abstraction
- Chipmunk2D - Physics
- ENet - Networking
- Soloud - Audio
- Tracy - Profiling (when enabled)
## Development Tips
### Running Games
```bash
# Build first
make debug
# Run example from build directory
./build_dbg/prosperon examples/chess
# Or copy to game directory
cp build_dbg/prosperon examples/chess/
cd examples/chess
./prosperon
```
### Documentation
- Documentation is found in docs
- Documentation for the JS modules loaded with 'use' is docs/api/modules
- .md files directly in docs gives a high level overview
- docs/dull is what this specific Javascript system is (including alterations from quickjs/es6)
### Shader Development
- Shaders are in `shaders/` directory as HLSL
- Compile script: `shaders/compile.sh`
- Outputs to platform-specific formats: `dxil/`, `msl/`, `spv/`
### Example Games
Located in `examples/` directory:
- `chess` - Chess implementation (has its own Makefile)
- `pong` - Classic pong game
- `snake` - Snake game
- `tetris` - Tetris clone
- `bunnymark` - Performance test
### Testing
```bash
# Run all tests
meson test -C build_dbg
# Run specific test
./build_dbg/prosperon tests/spawn_actor.js
```
### Debugging
- Use debug build: `make debug`
- Tracy profiler support when enabled
- Console logging available via `console.log()`, `console.error()`, etc.
- Log files written to `.prosperon/log.txt`
# Project Structure Notes
## Core JavaScript Modules
- JavaScript modules are defined using the MISTUSE macro in jsffi.c
- The `js_os_funcs`, `js_io_funcs`, etc. arrays define the available functions for each module
- New functions are added with MIST_FUNC_DEF(module, function, args_count)
## File I/O
- `io.slurp(path)` - Reads a file as text
- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer
- `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file
- `io.exists(path)` - Checks if a file exists
## Script Loading
- The `use(path)` function in engine.js loads JavaScript modules
- Script loading happens in prosperon.c and the engine.js script
- jsffi.c contains the C hooks for the QuickJS JavaScript engine
- Added functionality for bytecode compilation and loading:
- `os.compile_bytecode(source, filename)` - Compiles JS to bytecode, returns ArrayBuffer
- `os.eval_bytecode(bytecode)` - Evaluates bytecode from an ArrayBuffer
- `compile(scriptPath)` - Compiles a JS file to a .jso bytecode file
- Modified `use()` to check for .jso files before loading .js files
## QuickJS Bytecode API
- `JS_Eval` with JS_EVAL_FLAG_COMPILE_ONLY - Compiles without executing
- `JS_WriteObject` with JS_WRITE_OBJ_BYTECODE - Serializes to bytecode
- `JS_ReadObject` with JS_READ_OBJ_BYTECODE - Deserializes and loads bytecode
- Bytecode files use .jso extension alongside .js files
## Available JavaScript APIs
### Core APIs
- `actor` - Base prototype for all actor objects
- `$_` - Special global for actor messaging
- `prosperon` - Global engine interface
- `console` - Logging and debugging interface
### Framework APIs
- `moth` - Higher-level game framework that simplifies Prosperon usage
- Handles window creation, game loop, and event dispatching
- Provides simple configuration via config.js
- Auto-initializes systems like rendering and input
- Manages camera, resolution, and FPS automatically
### Rendering
- `draw2d` - 2D drawing primitives
- `render` - Low-level rendering operations
- `graphics` - Higher-level graphics utilities
- `camera` - Camera controls and transformations
- `sprite` - Sprite rendering and management
### Physics and Math
- `math` - Mathematical utilities
- `geometry` - Geometric calculations and shapes
- `transform` - Object transformations
### Input and Events
- `input` - Mouse, keyboard, and touch handling
- `event` - Event management system
### Networking
- `enet` - Networking through ENet library
- `http` - HTTP client capabilities
### Audio
- `sound` - Audio playback using SoLoud
### Utility Modules
- `time` - Time management and delays
- `io` - File I/O operations
- `json` - JSON parsing and serialization
- `util` - General utilities
- `color` - Color manipulation
- `miniz` - Compression utilities
- `nota` - Structured data format
- `wota` - Serialization format
- `qr` - QR code generation/reading
- `tween` - Animation tweening
- `spline` - Spline calculations
- `imgui` - Immediate mode GUI
## Game Development Patterns
### Project Structure
- Game config is typically in `config.js`
- Main entry point is `main.js`
- Resource loading through `resources.js`
### Actor Pattern Usage
- Create actors with `actor.spawn(script, config)`
- Start actors with `$_.start(callback, script)` - the system automatically sends a greeting, callback receives {type: 'greet', actor: actor_ref}
- No need to manually send greetings - `$_.start` handles this automatically
- Manage actor hierarchy with overlings and underlings
- Schedule actor tasks with `$_.delay()` method
- Clean up with `kill()` and `garbage()`
### Actor Messaging with Callbacks
When sending a message with a callback, respond by sending to the message itself:
```javascript
// Sender side:
send(actor, {type: 'status'}, response => {
console.log(response); // Handle the response
});
// Receiver side:
$_.receiver(msg => {
if (msg.type === 'status') {
send(msg, {status: 'ok'}); // Send response to the message itself
}
});
```
**Critical Rules for Message Callbacks**:
- **A message can only be used ONCE as a send target** - after sending a response to a message, it cannot be used again
- If you need to send multiple updates (like progress), only the download request message should be used for the final response
- Status requests should each get their own individual response
- Actor objects and message headers are completely opaque - never try to access internal properties
- Never access `msg.__HEADER__` or similar - the actor system handles routing internally
- Use `$_.delay()` to schedule work and avoid blocking the message receiver
### Game Loop Registration
- Register functions like `update`, `draw`, `gui`, etc.
- Set function.layer property to control execution order
- Use `Register` system to manage callbacks
### Program vs Module Pattern
- Programs are actor scripts that don't return values, they execute top-to-bottom
- Modules are files that return single values (usually objects) that get frozen
- Programs can spawn other programs as underlings
- Programs have lifecycle hooks: awake, update, draw, garbage, etc.
## Technical Capabilities
### Graphics Pipeline
- Supports multiple render backends (Direct3D, Metal, Vulkan via SDL3)
- Custom shader system with cross-platform compilation
- Sprite batching for efficient 2D rendering
- Camera systems for both 2D and 3D
### Asset Support
- Images: PNG, JPG, QOI, etc.
- Audio: Various formats through SoLoud
- Models: Basic 3D model support
- Custom formats: Aseprite animations, etc.
### Developer Tools
- Built-in documentation system with `prosperon.DOC`
- Tracy profiler integration for performance monitoring
- Imgui debugging tools
- Console logging with various severity levels
## Misty Networking Patterns
Prosperon implements the Misty actor networking model. Understanding these patterns is critical for building distributed applications.
### Portal Reply Pattern
Portals must reply with an actor object, not application data:
```javascript
// CORRECT: Portal replies with actor
$_.portal(e => {
send(e, $_); // Reply with server actor
}, 5678);
// WRONG: Portal sends application data
$_.portal(e => {
send(e, {type: 'game_start'}); // This breaks the pattern
}, 5678);
```
### Two-Phase Connection Protocol
Proper Misty networking follows a two-phase pattern:
**Phase 1: Actor Connection**
- Client contacts portal using `$_.contact()`
- Portal replies with an actor object
- This establishes the communication channel
**Phase 2: Application Communication**
- Client sends application messages to the received actor
- Normal bidirectional messaging begins
- Application logic handles game/service initialization
### Message Handling Best Practices
Messages should be treated as opaque objects with your application data:
```javascript
// CORRECT: Store actor references separately
var players = {};
$_.receiver(msg => {
if (msg.type === 'join_game' && msg.player_id) {
// Store the message for later response
players[msg.player_id] = msg;
// Later, respond to the stored message
send(players[msg.player_id], {type: 'game_start'});
}
});
// WRONG: Trying to access internal message properties
$_.receiver(msg => {
var sender = msg.__HEADER__.replycc; // Never do this!
});
```
### Return ID Lifecycle
- Each reply callback gets a unique return ID
- Return IDs are consumed once and then deleted
- Reusing message objects with return headers causes "Could not find return function" errors
- Always create clean actor references for ongoing communication
### Actor Object Transparency
Actor objects must be completely opaque black boxes that work identically regardless of transport:
```javascript
// Actor objects work transparently for:
// - Same-process communication (fastest - uses mailbox)
// - Inter-process communication (uses mailbox)
// - Network communication (uses ENet)
// The actor shouldn't know or care about the transport mechanism
send(opponent, {type: 'move', from: [0,0], to: [1,1]});
```
**Key Implementation Details:**
- `actor_send()` in `scripts/core/engine.js` handles routing based on available actor data
- Actor objects sent in message data automatically get address/port populated when received over network
- Three communication pathways: `os.mailbox_exist()` check → mailbox send → network send
- Actor objects must contain all necessary routing information for transparent messaging
### Common Networking Bugs
1. **Portal sending application data**: Portal should only establish actor connections
2. **Return ID collision**: Reusing messages with return headers for multiple sends
3. **Mixed phases**: Trying to do application logic during connection establishment
4. **Header pollution**: Using received message objects as actor references
5. **Missing actor address info**: Actor objects in message data need network address population (fixed in engine.js:746-766)
### Example: Correct Chess Networking
```javascript
// Server: Portal setup
$_.portal(e => {
send(e, $_); // Just reply with actor
}, 5678);
// Client: Two-phase connection
$_.contact((actor, reason) => {
if (actor) {
opponent = actor;
send(opponent, {type: 'join_game'}); // Phase 2: app messaging
}
}, {address: "localhost", port: 5678});
// Server: Handle application messages
$_.receiver(e => {
if (e.type === 'join_game') {
opponent = e.__HEADER__.replycc;
send(opponent, {type: 'game_start', your_color: 'black'});
}
});
```
## Memory Management
- When working with a conversational AI system like Claude, it's important to maintain a clean and focused memory
- Regularly review and update memories to ensure they remain relevant and helpful
- Delete or modify memories that are no longer accurate or useful
- Prioritize information that can genuinely assist in future interactions

54
Dockerfile Normal file
View File

@@ -0,0 +1,54 @@
# Builder stage
FROM ubuntu:plucky AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
libasound2-dev \
libpulse-dev \
libudev-dev \
libwayland-dev \
wayland-protocols \
libxkbcommon-dev \
libx11-dev \
libxext-dev \
libxrandr-dev \
libxcursor-dev \
libxi-dev \
libxinerama-dev \
libxss-dev \
libegl1-mesa-dev \
libgl1-mesa-dev \
cmake \
ninja-build \
git \
build-essential \
binutils \
pkg-config \
meson \
zip && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN git clone https://gitea.pockle.world/john/prosperon.git
WORKDIR /app/prosperon
RUN git checkout jsffi_refactor
RUN meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
RUN meson compile -C build
# Runtime stage
FROM ubuntu:latest
# Install minimal runtime dependencies (e.g., for dynamically linked libraries)
RUN apt-get update && apt-get install -y libstdc++6 && rm -rf /var/lib/apt/lists/*
# Copy the compiled prosperon binary from the build stage
COPY --from=builder /app/prosperon/build/prosperon /usr/local/bin/prosperon
# Create an entrypoint script
RUN echo '#!/bin/bash' > /entrypoint.sh && \
echo '/usr/local/bin/prosperon "$@" &' >> /entrypoint.sh && \
echo 'tail -f /dev/null' >> /entrypoint.sh && \
chmod +x /entrypoint.sh
WORKDIR /workdir
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,5 +1,5 @@
debug: FORCE debug: FORCE
meson setup build_dbg -Dbuildtype=debugoptimized meson setup build_dbg -Dbuildtype=debug
meson compile -C build_dbg meson compile -C build_dbg
fast: FORCE fast: FORCE
@@ -7,7 +7,7 @@ fast: FORCE
meson compile -C build_fast meson compile -C build_fast
release: FORCE release: FORCE
meson setup -Dbuildtype=release -Db_lto=true -Db_ndebug=true build_release meson setup -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true build_release
meson compile -C build_release meson compile -C build_release
sanitize: FORCE sanitize: FORCE

View File

@@ -1,9 +1,7 @@
Thank you for using Prosperon! Thank you for using Prosperon!
Provided are prosperon builds for all available platforms, including SDL3 for each respective one. SDL3 must be present in the same folder as prosperon to run! Provided are prosperon builds for all available platforms. Simply run prosperon for your platform in a game folder to play!
To get started, take a dive into the provided example games in the examples folder. Just copy the prosperon executable for your platform, along with its SDL3 library, into any provided example folder, then run it! To get started, take a dive into the provided example games in the examples folder. You can either copy the prosperon executable into an example directory and run it there, or run `prosperon path/to/example` from the project root.
NOTE: For MacOS, SDL3 must first be installed with homebrew. After installing homebrew, run `brew install sdl3`. This will be fixed in a future release!
You can take a look through the docs folder for the prosperon manual to learn all about it. The manual is available on the web at [docs.prosperon.dev](https://docs.prosperon.dev). You can take a look through the docs folder for the prosperon manual to learn all about it. The manual is available on the web at [docs.prosperon.dev](https://docs.prosperon.dev).

76
benchmarks/nota.js Normal file
View File

@@ -0,0 +1,76 @@
var nota = use('nota')
var os = use('os')
var io = use('io')
var ll = io.slurp('benchmarks/nota.json')
var newarr = []
var accstr = ""
for (var i = 0; i < 10000; i++) {
accstr += i;
newarr.push(i.toString())
}
// Arrays to store timing results
var jsonDecodeTimes = [];
var jsonEncodeTimes = [];
var notaEncodeTimes = [];
var notaDecodeTimes = [];
var notaSizes = [];
// Run 100 tests
for (let i = 0; i < 100; i++) {
// JSON Decode test
let start = os.now();
var jll = json.decode(ll);
jsonDecodeTimes.push((os.now() - start) * 1000);
// JSON Encode test
start = os.now();
let jsonStr = JSON.stringify(jll);
jsonEncodeTimes.push((os.now() - start) * 1000);
// NOTA Encode test
start = os.now();
var nll = nota.encode(jll);
notaEncodeTimes.push((os.now() - start) * 1000);
// NOTA Decode test
start = os.now();
var oll = nota.decode(nll);
notaDecodeTimes.push((os.now() - start) * 1000);
}
// Calculate statistics
function getStats(arr) {
const avg = arr.reduce((a, b) => a + b) / arr.length;
const min = Math.min(...arr);
const max = Math.max(...arr);
return { avg, min, max };
}
// Pretty print results
console.log("\n=== Performance Test Results (100 iterations) ===");
console.log("\nJSON Decoding (ms):");
const jsonDecStats = getStats(jsonDecodeTimes);
console.log(`Average: ${jsonDecStats.avg.toFixed(2)} ms`);
console.log(`Min: ${jsonDecStats.min.toFixed(2)} ms`);
console.log(`Max: ${jsonDecStats.max.toFixed(2)} ms`);
console.log("\nJSON Encoding (ms):");
const jsonEncStats = getStats(jsonEncodeTimes);
console.log(`Average: ${jsonEncStats.avg.toFixed(2)} ms`);
console.log(`Min: ${jsonEncStats.min.toFixed(2)} ms`);
console.log(`Max: ${jsonEncStats.max.toFixed(2)} ms`);
console.log("\nNOTA Encoding (ms):");
const notaEncStats = getStats(notaEncodeTimes);
console.log(`Average: ${notaEncStats.avg.toFixed(2)} ms`);
console.log(`Min: ${notaEncStats.min.toFixed(2)} ms`);
console.log(`Max: ${notaEncStats.max.toFixed(2)} ms`);
console.log("\nNOTA Decoding (ms):");
const notaDecStats = getStats(notaDecodeTimes);
console.log(`Average: ${notaDecStats.avg.toFixed(2)} ms`);
console.log(`Min: ${notaDecStats.min.toFixed(2)} ms`);
console.log(`Max: ${notaDecStats.max.toFixed(2)} ms`);

2132
benchmarks/nota.json Normal file

File diff suppressed because it is too large Load Diff

106
benchmarks/wota.js Normal file
View File

@@ -0,0 +1,106 @@
//
// wota_benchmark.js
//
// Usage in QuickJS:
// qjs wota_benchmark.js
//
// Prerequisite:
var wota = use('wota');
var os = use('os');
// or otherwise ensure `wota` and `os` are available.
// Make sure wota_benchmark.js is loaded after wota.js or combined with it.
//
// Helper to run a function repeatedly and measure total time in seconds.
// Returns elapsed time in seconds.
function measureTime(fn, iterations) {
let t1 = os.now();
for (let i = 0; i < iterations; i++) {
fn();
}
let t2 = os.now();
return t2 - t1;
}
// We'll define a function that does `encode -> decode` for a given value:
function roundTripWota(value) {
let encoded = wota.encode(value);
let decoded = wota.decode(encoded);
// Not doing a deep compare here, just measuring performance.
// (We trust the test suite to verify correctness.)
}
// A small suite of data we want to benchmark. Each entry includes:
// name: label for printing
// data: the test value(s) to encode/decode
// iterations: how many times to loop
//
// You can tweak these as you like for heavier or lighter tests.
const benchmarks = [
{
name: "Small Integers",
data: [0, 42, -1, 2023],
iterations: 100000
},
{
name: "Strings (short, emoji)",
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
iterations: 100000
},
{
name: "Small Objects",
data: [
{ a:1, b:2.2, c:"3", d:false },
{ x:42, y:null, z:"test" }
],
iterations: 50000
},
{
name: "Nested Arrays",
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
iterations: 50000
},
{
name: "Large Array (1k numbers)",
// A thousand random numbers
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
iterations: 1000
},
{
name: "Large Binary Blob (256KB)",
// A 256KB ArrayBuffer
data: [ new Uint8Array(256 * 1024).buffer ],
iterations: 200
}
];
// Print a header
console.log("Wota Encode/Decode Benchmark");
console.log("============================\n");
// We'll run each benchmark scenario in turn.
for (let bench of benchmarks) {
// We'll measure how long it takes to do 'iterations' *for each test value*
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
// Then we compute an overall encode+decode throughput (ops/s).
let totalIterations = bench.iterations * bench.data.length;
// We'll define a function that does a roundTrip for *each* data item in bench.data
// to measure in one loop iteration. Then we multiply by bench.iterations.
function runAllData() {
for (let val of bench.data) {
roundTripWota(val);
}
}
let elapsedSec = measureTime(runAllData, bench.iterations);
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
console.log(`${bench.name}:`);
console.log(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
console.log(` Elapsed: ${elapsedSec.toFixed(3)} s`);
console.log(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
}
// All done
console.log("Benchmark completed.\n");

View File

@@ -0,0 +1,182 @@
//
// benchmark_wota_nota_json.js
//
// Usage in QuickJS:
// qjs benchmark_wota_nota_json.js
//
// Ensure wota, nota, json, and os are all available, e.g.:
var wota = use('wota');
var nota = use('nota');
var json = use('json');
var os = use('os');
//
////////////////////////////////////////////////////////////////////////////////
// 1. Setup "libraries" array to easily switch among Wota, Nota, and JSON
////////////////////////////////////////////////////////////////////////////////
const libraries = [
{
name: "Wota",
encode: wota.encode,
decode: wota.decode,
// Wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
getSize(encoded) {
return encoded.byteLength;
}
},
{
name: "Nota",
encode: nota.encode,
decode: nota.decode,
// Nota also produces an ArrayBuffer:
getSize(encoded) {
return encoded.byteLength;
}
},
{
name: "JSON",
encode: json.encode,
decode: json.decode,
// JSON produces a JS string. We'll measure its UTF-16 code unit length
// as a rough "size". Alternatively, you could convert to UTF-8 for
// a more accurate byte size. Here we just use `string.length`.
getSize(encodedStr) {
return encodedStr.length;
}
}
];
////////////////////////////////////////////////////////////////////////////////
// 2. Test data sets (similar to wota benchmarks).
// Each scenario has { name, data, iterations }
////////////////////////////////////////////////////////////////////////////////
const benchmarks = [
{
name: "Small Integers",
data: [0, 42, -1, 2023],
iterations: 100000
},
{
name: "Floating point",
data: [0.1, 1e-50, 3.14159265359],
iterations: 100000
},
{
name: "Strings (short, emoji)",
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
iterations: 100000
},
{
name: "Small Objects",
data: [
{ a:1, b:2.2, c:"3", d:false },
{ x:42, y:null, z:"test" }
],
iterations: 50000
},
{
name: "Nested Arrays",
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
iterations: 50000
},
{
name: "Large Array (1k integers)",
data: [ Array.from({length:1000}, (_, i) => i) ],
iterations: 1000
},
{
name: "Large Binary Blob (256KB)",
data: [ new Uint8Array(256 * 1024).buffer ],
iterations: 200
}
];
////////////////////////////////////////////////////////////////////////////////
// 3. Utility: measureTime(fn) => how long fn() takes in seconds.
////////////////////////////////////////////////////////////////////////////////
function measureTime(fn) {
let start = os.now();
fn();
let end = os.now();
return (end - start); // in seconds
}
////////////////////////////////////////////////////////////////////////////////
// 4. For each library, we run each benchmark scenario and measure:
// - Encoding time (seconds)
// - Decoding time (seconds)
// - Total encoded size (bytes or code units for JSON)
//
////////////////////////////////////////////////////////////////////////////////
function runBenchmarkForLibrary(lib, bench) {
// We'll encode and decode each item in `bench.data`.
// We do 'bench.iterations' times. Then sum up total time.
// Pre-store the encoded results for all items so we can measure decode time
// in a separate pass. Also measure total size once.
let encodedList = [];
let totalSize = 0;
// 1) Measure ENCODING
let encodeTime = measureTime(() => {
for (let i = 0; i < bench.iterations; i++) {
// For each data item, encode it
for (let d of bench.data) {
let e = lib.encode(d);
// store only in the very first iteration, so we can decode them later
// but do not store them every iteration or we blow up memory.
if (i === 0) {
encodedList.push(e);
totalSize += lib.getSize(e);
}
}
}
});
// 2) Measure DECODING
let decodeTime = measureTime(() => {
for (let i = 0; i < bench.iterations; i++) {
// decode everything we stored during the first iteration
for (let e of encodedList) {
let decoded = lib.decode(e);
// not verifying correctness here, just measuring speed
}
}
});
return { encodeTime, decodeTime, totalSize };
}
////////////////////////////////////////////////////////////////////////////////
// 5. Main driver: run across all benchmarks, for each library.
////////////////////////////////////////////////////////////////////////////////
console.log("Benchmark: Wota vs Nota vs JSON");
console.log("================================\n");
for (let bench of benchmarks) {
console.log(`SCENARIO: ${bench.name}`);
console.log(` Data length: ${bench.data.length} | Iterations: ${bench.iterations}\n`);
for (let lib of libraries) {
let { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
// We'll compute total operations = bench.iterations * bench.data.length
let totalOps = bench.iterations * bench.data.length;
let encOpsPerSec = (totalOps / encodeTime).toFixed(1);
let decOpsPerSec = (totalOps / decodeTime).toFixed(1);
console.log(` ${lib.name}:`);
console.log(` Encode time: ${encodeTime.toFixed(3)}s => ${encOpsPerSec} encodes/sec`);
console.log(` Decode time: ${decodeTime.toFixed(3)}s => ${decOpsPerSec} decodes/sec`);
console.log(` Total size: ${totalSize} bytes (or code units for JSON)`);
console.log("");
}
console.log("---------------------------------------------------------\n");
}
console.log("Benchmark complete.\n");

View File

@@ -0,0 +1,46 @@
# camera
### list() <sub>function</sub>
Return an array of available camera device IDs.
**Returns**: An array of camera IDs, or undefined if no cameras are available.
### open(id) <sub>function</sub>
Open a camera device with the given ID.
**id**: The camera ID to open.
**Returns**: A camera object on success, or throws an error if the camera cannot be opened.
### name(id) <sub>function</sub>
Return the name of the camera with the given ID.
**id**: The camera ID to query.
**Returns**: A string with the camera's name, or throws an error if the name cannot be retrieved.
### position(id) <sub>function</sub>
Return the physical position of the camera with the given ID.
**id**: The camera ID to query.
**Returns**: A string indicating the camera position ("unknown", "front", or "back").

76
docs/api/modules/debug.md Normal file
View File

@@ -0,0 +1,76 @@
# debug
### stack_depth() <sub>function</sub>
Return the current stack depth.
**Returns**: A number representing the stack depth.
### build_backtrace() <sub>function</sub>
Build and return a backtrace of the current call stack.
**Returns**: An object representing the call stack backtrace.
### closure_vars(fn) <sub>function</sub>
Return the closure variables for a given function.
**fn**: The function object to inspect.
**Returns**: An object containing the closure variables.
### local_vars(depth) <sub>function</sub>
Return the local variables for a specific stack frame.
**depth**: The stack frame depth to inspect.
**Returns**: An object containing the local variables at the specified depth.
### fn_info(fn) <sub>function</sub>
Return metadata about a given function.
**fn**: The function object to inspect.
**Returns**: An object with metadata about the function.
### backtrace_fns() <sub>function</sub>
Return an array of functions in the current backtrace.
**Returns**: An array of function objects from the call stack.
### dump_obj(obj) <sub>function</sub>
Return a string representation of a given object.
**obj**: The object to dump.
**Returns**: A string describing the object's contents.

39
docs/api/modules/dmon.md Normal file
View File

@@ -0,0 +1,39 @@
# dmon
### watch() <sub>function</sub>
Start watching the root directory, recursively.
This function begins monitoring the specified directory and its subdirectories recursively for events such as file creation, deletion, modification, or movement. Events are queued and can be retrieved by calling poll.
:throws: An error if dmon is already watching.
**Returns**: None
### unwatch() <sub>function</sub>
Stop watching the currently monitored directory.
This function halts filesystem monitoring for the directory previously set by watch. It clears the watch state, allowing a new watch to be started.
:throws: An error if no directory is currently being watched.
**Returns**: None
### poll(callback) <sub>function</sub>
Retrieve and process queued filesystem events.
This function dequeues all pending filesystem events and invokes the provided callback for each one. The callback receives an event object with properties: 'action' (string: "create", "delete", "modify", or "move"), 'root' (string: watched directory), 'file' (string: affected file path), and 'old' (string: previous file path for move events, empty if not applicable).
**callback**: A function to call for each event, receiving an event object as its argument.
**Returns**: None

45
docs/api/modules/enet.md Normal file
View File

@@ -0,0 +1,45 @@
# enet
### initialize() <sub>function</sub>
Initialize the ENet library. Must be called before using any ENet functionality.
Throws an error if initialization fails.
**Returns**: None
### deinitialize() <sub>function</sub>
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
need any ENet functionality.
**Returns**: None
### create_host(address) <sub>function</sub>
Create an ENet host for either a client-like unbound host or a server bound to a specific
address and port:
- If no argument is provided, creates an unbound "client-like" host with default settings
(maximum 32 peers, 2 channels, unlimited bandwidth).
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
Throws an error if host creation fails for any reason.
omit to create an unbound client-like host.
**address**: (optional) A string in 'ip:port' format to bind the host (server), or
**Returns**: An ENetHost object.

File diff suppressed because it is too large Load Diff

View File

@@ -66,23 +66,3 @@ and booleans for pressed buttons (left, middle, right, x1, x2).
**Returns**: Object { x, y, left, middle, right, x1, x2 } **Returns**: Object { x, y, left, middle, right, x1, x2 }
### mouse <sub>object</sub>
### keyboard <sub>object</sub>
### print_pawn_kbm(pawn) <sub>function</sub>
### procdown() <sub>function</sub>
### print_md_kbm(pawn) <sub>function</sub>
### has_bind(pawn, bind) <sub>function</sub>
### action <sub>object</sub>
### tabcomplete(val, list) <sub>function</sub>
### do_uncontrol(pawn) <sub>function</sub>
### player <sub>object</sub>

View File

@@ -174,12 +174,17 @@ Return the application's base directory (where the executable is located).
**Returns**: A string with the base directory path. **Returns**: A string with the base directory path.
### userdir() <sub>function</sub> ### prefdir(org, app) <sub>function</sub>
Return the user's directory, often used for saving data. Get the user-and-app-specific path where files can be written.
**org**: The name of your organization.
**app**: The name of your application.
**Returns**: A string with the user's directory path. **Returns**: A string with the user's directory path.

30
docs/api/modules/nota.md Normal file
View File

@@ -0,0 +1,30 @@
# nota
### encode(value) <sub>function</sub>
Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
:throws: An error if no argument is provided.
**value**: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
**Returns**: An ArrayBuffer containing the NOTA-encoded data.
### decode(buffer) <sub>function</sub>
Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
**buffer**: An ArrayBuffer containing NOTA-encoded data to decode.
**Returns**: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.

View File

@@ -33,71 +33,3 @@
### imgui(...args) <sub>function</sub> ### imgui(...args) <sub>function</sub>
### app(...args) <sub>function</sub> ### app(...args) <sub>function</sub>
### date <sub>string</sub>
### camera <sub>object</sub>
### debug <sub>boolean</sub>
### semver <sub>object</sub>
### title <sub>string</sub>
### width <sub>number</sub>
### height <sub>number</sub>
### size <sub>object</sub>
### icon <sub>object</sub>
### high_dpi <sub>number</sub>
### alpha <sub>number</sub>
### fullscreen <sub>number</sub>
### sample_count <sub>number</sub>
### enable_clipboard <sub>boolean</sub>
### enable_dragndrop <sub>boolean</sub>
### max_dropped_files <sub>number</sub>
### swap_interval <sub>number</sub>
### name <sub>string</sub>
### identifier <sub>string</sub>
### creator <sub>string</sub>
### copyright <sub>string</sub>
### type <sub>string</sub>
### url <sub>string</sub>
### postvals <sub>object</sub>
### hudcam <sub>object</sub>
### appcam <sub>object</sub>
### screencolor <sub>object</sub>
### window <sub>object</sub>
An application window, created via prosperon.engine_start or SDL calls. Freed on GC.
### font <sub>object</sub>
### gpu <sub>object</sub>
A handle for low-level GPU operations via SDL GPU. Freed on GC.
### exit() <sub>function</sub>

View File

@@ -0,0 +1,66 @@
# enet_host
### service(callback, timeout) <sub>function</sub>
Poll for and process any available network events (connect, receive, disconnect, or none)
from this host, calling the provided callback for each event. This function loops until
no more events are available in the current timeframe.
Event object properties:
- type: String, one of "connect", "receive", "disconnect", or "none".
- peer: (present if type = "connect") The ENetPeer object for the new connection.
- channelID: (present if type = "receive") The channel on which the data was received.
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
object as its single argument.
**callback**: A function called once for each available event, receiving an event
**timeout**: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
**Returns**: None
### connect(host, port) <sub>function</sub>
Initiate a connection from this host to a remote server. Throws an error if the
connection cannot be started.
**host**: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
**port**: The port number to connect to.
**Returns**: An ENetPeer object representing the connection.
### flush() <sub>function</sub>
Flush all pending outgoing packets for this host immediately.
**Returns**: None
### broadcast(data) <sub>function</sub>
Broadcast a JavaScript object to all connected peers on channel 0. The object is
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
**data**: A JavaScript object to broadcast to all peers.
**Returns**: None

102
docs/api/types/enet_peer.md Normal file
View File

@@ -0,0 +1,102 @@
# enet_peer
### send(data) <sub>function</sub>
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
sent reliably. Throws an error if serialization fails.
**data**: A JavaScript object to send.
**Returns**: None
### disconnect() <sub>function</sub>
Request a graceful disconnection from this peer. The connection will close after
pending data is sent.
**Returns**: None
### disconnect_now() <sub>function</sub>
Immediately terminate the connection to this peer, discarding any pending data.
**Returns**: None
### disconnect_later() <sub>function</sub>
Request a disconnection from this peer after all queued packets are sent.
**Returns**: None
### reset() <sub>function</sub>
Reset this peer's connection, immediately dropping it and clearing its internal state.
**Returns**: None
### ping() <sub>function</sub>
Send a ping request to this peer to measure latency.
**Returns**: None
### throttle_configure(interval, acceleration, deceleration) <sub>function</sub>
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
rate based on packet loss or congestion.
**interval**: The interval (ms) between throttle adjustments.
**acceleration**: The factor to increase sending speed when conditions improve.
**deceleration**: The factor to decrease sending speed when conditions worsen.
**Returns**: None
### timeout(timeout_limit, timeout_min, timeout_max) <sub>function</sub>
Set timeout parameters for this peer, determining how long ENet waits before considering
the connection lost.
**timeout_limit**: The total time (ms) before the peer is disconnected.
**timeout_min**: The minimum timeout (ms) used for each timeout attempt.
**timeout_max**: The maximum timeout (ms) used for each timeout attempt.
**Returns**: None

661
docs/dull/Array.md Normal file
View File

@@ -0,0 +1,661 @@
# Array
### length <sub>number</sub>
### at(index) <sub>function</sub>
Return the item at index 'index', supporting negative indices to count from
the end. If 'index' is out of range, returns undefined.
**index**: The index of the element to return (can be negative).
**Returns**: The element at the given index, or undefined.
### with(index, value) <sub>function</sub>
Return a shallow copy of the array, but with the element at 'index' replaced
by 'value'. If 'index' is negative, it counts from the end. Throws if out of range.
**index**: The zero-based index (can be negative) to replace.
**value**: The new value for the specified position.
**Returns**: A new array with the updated element.
### concat(items) <sub>function</sub>
Return a new array that is the result of concatenating this array with
any additional arrays or values provided.
**items**: One or more arrays or values to concatenate.
**Returns**: A new array with the items appended.
### every(callback, thisArg) <sub>function</sub>
Return true if the provided callback function returns a truthy value for
every element in the array; otherwise false.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: True if all elements pass the test, otherwise false.
### some(callback, thisArg) <sub>function</sub>
Return true if the provided callback function returns a truthy value for at
least one element in the array; otherwise false.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: True if at least one element passes the test, otherwise false.
### forEach(callback, thisArg) <sub>function</sub>
Call the provided callback function once for each element in the array.
Does not produce a return value.
**callback**: A function(element, index, array).
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: None
### map(callback, thisArg) <sub>function</sub>
Create a new array with the results of calling a provided callback function
on every element in this array.
**callback**: A function(element, index, array) => newElement.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: A new array of transformed elements.
### filter(callback, thisArg) <sub>function</sub>
Create a new array containing all elements for which the provided callback
function returns a truthy value.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: A new array of elements that passed the test.
### reduce(callback, initialValue) <sub>function</sub>
Apply a callback function against an accumulator and each element in the
array (from left to right) to reduce it to a single value.
**callback**: A function(accumulator, element, index, array) => newAccumulator.
**initialValue**: Optional. The initial value for the accumulator.
**Returns**: The single resulting value.
### reduceRight(callback, initialValue) <sub>function</sub>
Similar to reduce(), except it processes elements from right to left.
**callback**: A function(accumulator, element, index, array) => newAccumulator.
**initialValue**: Optional. The initial value for the accumulator.
**Returns**: The single resulting value.
### fill(value, start, end) <sub>function</sub>
Fill the array with a static value from 'start' index up to (but not including)
'end' index. Modifies the original array.
**value**: The value to fill.
**start**: The starting index (default 0).
**end**: The ending index (default array.length).
**Returns**: The modified array (with filled values).
### find(callback, thisArg) <sub>function</sub>
Return the first element in the array that satisfies the provided callback
function. If none is found, return undefined.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: The first matching element, or undefined if not found.
### findIndex(callback, thisArg) <sub>function</sub>
Return the index of the first element in the array that satisfies the
provided callback function. If none is found, return -1.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: The index of the first matching element, or -1 if not found.
### findLast(callback, thisArg) <sub>function</sub>
Return the last element in the array that satisfies the provided callback
function, searching from right to left. If none is found, return undefined.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: The last matching element, or undefined if not found.
### findLastIndex(callback, thisArg) <sub>function</sub>
Return the index of the last element in the array that satisfies the
provided callback function, searching from right to left. If none is found,
return -1.
**callback**: A function(element, index, array) => boolean.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: The index of the last matching element, or -1 if not found.
### indexOf(searchElement, fromIndex) <sub>function</sub>
Return the first index at which a given element can be found. If not present,
return -1.
**searchElement**: The item to locate in the array.
**fromIndex**: The index at which to start searching (default 0).
**Returns**: The index of the found element, or -1 if not found.
### lastIndexOf(searchElement, fromIndex) <sub>function</sub>
Return the last index at which a given element can be found, or -1 if not
present. Searches backward from 'fromIndex'.
**searchElement**: The item to locate in the array.
**fromIndex**: The index at which to start searching backward (default array.length - 1).
**Returns**: The last index of the found element, or -1 if not found.
### includes(searchElement, fromIndex) <sub>function</sub>
Return a boolean indicating whether the array contains the given element,
comparing elements using the SameValueZero algorithm.
**searchElement**: The value to search for.
**fromIndex**: The position in the array to start searching (default 0).
**Returns**: True if the element is found, otherwise false.
### join(separator) <sub>function</sub>
Join all elements of the array into a string, separated by 'separator'.
**separator**: The delimiter string to separate elements (default ',').
**Returns**: A string of array elements joined by the separator.
### toString() <sub>function</sub>
Return a string representing the elements of the array, separated by commas.
Overrides Object.prototype.toString.
**Returns**: A comma-separated string of array elements.
### toLocaleString() <sub>function</sub>
Return a localized string representing the array and its elements, calling
each element's toLocaleString if available.
**Returns**: A locale-sensitive, comma-separated string of array elements.
### pop() <sub>function</sub>
Remove the last element from the array and return it. This changes the length
of the array.
**Returns**: The removed element, or undefined if the array is empty.
### push(items) <sub>function</sub>
Append one or more elements to the end of the array and return the new length
of the array.
**items**: One or more items to append.
**Returns**: The new length of the array.
### shift() <sub>function</sub>
Remove the first element from the array and return it. This changes the length
of the array.
**Returns**: The removed element, or undefined if the array is empty.
### unshift(items) <sub>function</sub>
Insert one or more elements at the start of the array and return the new
length of the array.
**items**: One or more elements to insert at the start.
**Returns**: The new length of the array.
### reverse() <sub>function</sub>
Reverse the elements of the array in place and return the modified array.
**Returns**: The reversed array.
### toReversed() <sub>function</sub>
Return a shallow copy of the array in reverse order, without modifying the original array.
**Returns**: A new reversed array.
### sort(compareFunction) <sub>function</sub>
Sort the array in place, returning it. By default, sorts elements as strings in ascending order. An optional compareFunction may be used for custom sorting.
**compareFunction**: A function(a, b) => number, defining sort order.
**Returns**: The sorted array.
### toSorted(compareFunction) <sub>function</sub>
Return a shallow copy of the array, sorted according to the optional compare
function, without modifying the original array.
**compareFunction**: A function(a, b) => number, defining sort order.
**Returns**: A new sorted array.
### slice(start, end) <sub>function</sub>
Return a shallow copy of a portion of the array into a new array object, selected from 'start' to 'end' (end not included).
**start**: The beginning index (0-based). Negative values count from the end.
**end**: The end index (exclusive). Negative values count from the end.
**Returns**: A new array containing the extracted elements.
### splice(start, deleteCount, items) <sub>function</sub>
Change the contents of the array by removing or replacing existing elements and/or adding new elements in place. Returns an array of removed elements.
**start**: The index at which to start changing the array.
**deleteCount**: Number of elements to remove.
**items**: Elements to add in place of the removed elements.
**Returns**: An array containing the removed elements (if any).
### toSpliced(start, deleteCount, items) <sub>function</sub>
Return a shallow copy of the array, with a splice-like operation applied at 'start' removing 'deleteCount' elements and adding 'items'. Does not mutate the original array.
**start**: The index at which to start the splice operation.
**deleteCount**: Number of elements to remove.
**items**: Elements to add in place of the removed elements.
**Returns**: A new array with the splice applied.
### copyWithin(target, start, end) <sub>function</sub>
Copy a sequence of array elements within the array, overwriting existing values. This operation is performed in place and returns the modified array.
**target**: The index at which to copy the sequence to.
**start**: The beginning index of the sequence to copy.
**end**: The end index (exclusive) of the sequence to copy (default array.length).
**Returns**: The modified array.
### flatMap(callback, thisArg) <sub>function</sub>
Return a new array formed by applying a callback function to each element and then flattening the result by one level.
**callback**: A function(element, index, array) => array or value.
**thisArg**: Optional. A value to use as 'this' within the callback.
**Returns**: A new array with the flattened results.
### flat(depth) <sub>function</sub>
Return a new array with all sub-array elements concatenated into it recursively up to the specified depth.
**depth**: The maximum depth to flatten (default 1).
**Returns**: A new flattened array.
### values() <sub>function</sub>
Return a new Array Iterator object that contains the values for each index in the array.
**Returns**: An iterator over the array's elements.
### keys() <sub>function</sub>
Return a new Array Iterator object that contains the keys (indexes) for each index in the array.
**Returns**: An iterator over the array's indices.
### entries() <sub>function</sub>
Return a new Array Iterator object that contains key/value pairs for each index in the array. Each entry is [index, value].
**Returns**: An iterator over [index, value] pairs.
### add(other) <sub>function</sub>
Non-standard. Add corresponding elements of the current array and 'other' element-wise, returning a new array. Behavior depends on data types.
**other**: Another array or scalar to add to each element.
**Returns**: A new array with element-wise sum results.
### sub(other) <sub>function</sub>
Non-standard. Subtract corresponding elements of 'other' from the current array element-wise, returning a new array. Behavior depends on data types.
**other**: Another array or scalar to subtract from each element.
**Returns**: A new array with element-wise difference results.
### div(other) <sub>function</sub>
Non-standard. Divide each element of the current array by the corresponding element of 'other' (or a scalar) element-wise, returning a new array. Behavior depends on data types.
**other**: Another array or scalar for the division.
**Returns**: A new array with element-wise division results.
### scale() <sub>function</sub>
### lerp() <sub>function</sub>
### x <sub>accessor</sub>
### y <sub>accessor</sub>
### xy <sub>accessor</sub>
### r <sub>accessor</sub>
### g <sub>accessor</sub>
### b <sub>accessor</sub>
### a <sub>accessor</sub>
### rgb <sub>accessor</sub>
### rgba <sub>accessor</sub>
### filter!(fn) <sub>function</sub>
Perform an in-place filter of this array using the provided callback 'fn'.
Any element for which 'fn' returns a falsy value is removed. The array is modified
and then returned.
**fn**: A callback function(element, index, array) => boolean.
**Returns**: The filtered (modified) array.
### delete(item) <sub>function</sub>
Remove the first occurrence of 'item' from the array, if it exists.
Returns undefined.
**item**: The item to remove.
**Returns**: undefined
### copy() <sub>function</sub>
Return a deep copy of this array by applying 'deep_copy' to each element.
The resulting array is entirely new.
**Returns**: A new array that is a deep copy of the original.
### equal(b) <sub>function</sub>
Check if this array and array 'b' have the same elements in the same order.
If they are of different lengths, return false. Otherwise compare them via JSON.
**b**: Another array to compare against.
**Returns**: True if they match, false otherwise.
### last() <sub>function</sub>
Return the last element of this array. If the array is empty, returns undefined.
**Returns**: The last element of the array, or undefined if empty.
### wrapped(x) <sub>function</sub>
Return a copy of the array with the first 'x' elements appended to the end.
Does not modify the original array.
**x**: The number of leading elements to re-append.
**Returns**: A new array with the leading elements wrapped to the end.
### wrap_idx(x) <sub>function</sub>
Wrap the integer 'x' around this array's length, ensuring the resulting index
lies within [0, this.length - 1].
**x**: The index to wrap.
**Returns**: A wrapped index within this array's bounds.
### mirrored(x) <sub>function</sub>
Return a new array that appends a reversed copy (excluding the last element)
of itself to the end. For example, [1,2,3] -> [1,2,3,2,1]. If the array has length
<= 1, a copy of it is returned directly.
**Returns**: A new "mirrored" array.

View File

@@ -3,9 +3,20 @@ c = 'emcc'
cpp = 'em++' cpp = 'em++'
ar = 'emar' ar = 'emar'
strip = 'emstrip' strip = 'emstrip'
pkg-config = 'pkg-config'
exe_wrapper = 'node'
[host_machine] [host_machine]
system = 'emscripten' system = 'emscripten'
cpu_family = 'wasm64' cpu_family = 'wasm32'
cpu = 'wasm64' cpu = 'wasm32'
endian = 'little' endian = 'little'
[built-in options]
pkg_config_path = '$EMSDK/upstream/emscripten/cache/sysroot/lib/pkgconfig'
cmake_prefix_path = '$EMSDK/upstream/emscripten/cache/sysroot'
[properties]
needs_exe_wrapper = true
cmake_system_name = 'Emscripten'
sys_root = '@env:EMSDK@/upstream/emscripten/cache/sysroot'

View File

@@ -0,0 +1,8 @@
var config = {
title: "Box2D Physics Demo",
width: 1280,
height: 720,
fullscreen: false
}
return config

343
examples/box2d_demo/main.js Normal file
View File

@@ -0,0 +1,343 @@
var box2d = use('box2d')
var moth = use('moth', $_.delay)
moth.initialize()
var draw2d = use('draw2d')
// Physics world setup
var world = new box2d.World({
gravity: {x: 0, y: -10}
})
// Ground body (static)
var ground = world.createBody({
type: 'static',
position: {x: 0, y: -10}
})
var groundShape = ground.createBoxShape({
width: 50,
height: 0.5,
density: 0,
friction: 0.7
})
// Walls
var leftWall = world.createBody({
type: 'static',
position: {x: -25, y: 0}
})
leftWall.createBoxShape({
width: 0.5,
height: 30,
density: 0
})
var rightWall = world.createBody({
type: 'static',
position: {x: 25, y: 0}
})
rightWall.createBoxShape({
width: 0.5,
height: 30,
density: 0
})
// Dynamic bodies array
var boxes = []
var circles = []
// Create some dynamic boxes
for (var i = 0; i < 5; i++) {
var box = world.createBody({
type: 'dynamic',
position: {x: -10 + i * 4, y: 5 + i * 3},
angle: Math.random() * Math.PI
})
box.createBoxShape({
width: 2,
height: 2,
density: 1.0,
friction: 0.3,
restitution: 0.5
})
boxes.push(box)
}
// Create some circles
for (var i = 0; i < 3; i++) {
var circle = world.createBody({
type: 'dynamic',
position: {x: 5 + i * 3, y: 10 + i * 2}
})
circle.createCircleShape({
radius: 1,
density: 0.5,
friction: 0.2,
restitution: 0.8
})
circles.push(circle)
}
// Connected bodies with distance joint
var bodyA = world.createBody({
type: 'dynamic',
position: {x: -5, y: 15}
})
bodyA.createCircleShape({
radius: 0.5,
density: 1.0
})
var bodyB = world.createBody({
type: 'dynamic',
position: {x: 0, y: 15}
})
bodyB.createCircleShape({
radius: 0.5,
density: 1.0
})
var joint = world.createDistanceJoint({
bodyA: bodyA,
bodyB: bodyB,
localAnchorA: {x: 0, y: 0},
localAnchorB: {x: 0, y: 0},
length: 5
})
// Mouse interaction
var mouseBody = null
var mousePressed = false
// Game state
var camera = {x: 0, y: 0, zoom: 10}
// Input state
var keys = {}
var mouse = {
pos: {x: 0, y: 0},
buttons: [false, false, false]
}
// Main update function
prosperon.on('update', function(dt) {
// Step physics simulation
world.step(dt, 4)
// Camera controls
if (keys[4]) camera.x -= 20 * dt // A
if (keys[7]) camera.x += 20 * dt // D
if (keys[26]) camera.y += 20 * dt // W
if (keys[22]) camera.y -= 20 * dt // S
if (keys[20]) camera.zoom *= 1 + dt // Q
if (keys[8]) camera.zoom *= 1 - dt // E
// Mouse interaction
var mouseWorld = screenToWorld(mouse.pos)
// Raycast to find body under mouse
var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1)
if (!result.hit) {
// Create new body at mouse position
if (keys[225]) { // LSHIFT
// Create circle
var newCircle = world.createBody({
type: 'dynamic',
position: mouseWorld
})
newCircle.createCircleShape({
radius: 0.5 + Math.random(),
density: 1.0,
restitution: 0.3 + Math.random() * 0.5
})
circles.push(newCircle)
} else {
// Create box
var newBox = world.createBody({
type: 'dynamic',
position: mouseWorld,
angle: Math.random() * Math.PI * 2
})
newBox.createBoxShape({
width: 1 + Math.random() * 2,
height: 1 + Math.random() * 2,
density: 1.0,
restitution: 0.2 + Math.random() * 0.3
})
boxes.push(newBox)
}
}
boxes.forEach(function(box) {
var dir = {
x: box.position.x - mouseWorld.x,
y: box.position.y - mouseWorld.y
}
var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y)
if (dist < 10 && dist > 0.1) {
dir.x /= dist
dir.y /= dist
box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50})
}
})
// Reset with R
if (keys[21] && !keys[21 + '_prev']) { // R key pressed
// Reset all dynamic bodies
boxes.forEach(function(box) {
box.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10}
box.angle = Math.random() * Math.PI * 2
box.linearVelocity = {x: 0, y: 0}
box.angularVelocity = 0
})
circles.forEach(function(circle) {
circle.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10}
circle.linearVelocity = {x: 0, y: 0}
})
}
// Update previous key states
for (var k in keys) {
if (k.indexOf('_prev') === -1) {
keys[k + '_prev'] = keys[k]
}
}
})
// Event handlers
prosperon.on('key_down', function(e) {
keys[e.scancode] = true
})
prosperon.on('key_up', function(e) {
keys[e.scancode] = false
})
prosperon.on('mouse_button_down', function(e) {
mouse.buttons[e.which] = true
if (e.which === 0 && !mousePressed) {
mousePressed = true
var mouseWorld = screenToWorld(mouse.pos)
// Raycast to find body under mouse
var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1)
if (!result.hit) {
// Create new body at mouse position
if (keys[225]) { // LSHIFT
// Create circle
var newCircle = world.createBody({
type: 'dynamic',
position: mouseWorld
})
newCircle.createCircleShape({
radius: 0.5 + Math.random(),
density: 1.0,
restitution: 0.3 + Math.random() * 0.5
})
circles.push(newCircle)
} else {
// Create box
var newBox = world.createBody({
type: 'dynamic',
position: mouseWorld,
angle: Math.random() * Math.PI * 2
})
newBox.createBoxShape({
width: 1 + Math.random() * 2,
height: 1 + Math.random() * 2,
density: 1.0,
restitution: 0.2 + Math.random() * 0.3
})
boxes.push(newBox)
}
}
}
if (e.which === 1) {
// Apply impulse with right click
var mouseWorld = screenToWorld(mouse.pos)
boxes.forEach(function(box) {
var dir = {
x: box.position.x - mouseWorld.x,
y: box.position.y - mouseWorld.y
}
var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y)
if (dist < 10 && dist > 0.1) {
dir.x /= dist
dir.y /= dist
box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50})
}
})
}
})
prosperon.on('mouse_button_up', function(e) {
mouse.buttons[e.which] = false
if (e.which === 0) {
mousePressed = false
}
})
prosperon.on('mouse_motion', function(e) {
mouse.pos = e.pos
})
// Rendering
prosperon.on('draw', function() {
// Clear background
// Draw ground
drawBox(ground.position, 50, 0.5, ground.angle, {r: 0.5, g: 0.5, b: 0.5, a: 1})
// Draw walls
drawBox(leftWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1})
drawBox(rightWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1})
// Draw boxes
boxes.forEach(function(box) {
drawBox(box.position, 2, 2, box.angle, {r: 0.8, g: 0.3, b: 0.3, a: 1})
})
// Draw circles
circles.forEach(function(circle) {
draw2d.circle(circle.position, 1, {r: 0.3, g: 0.8, b: 0.3, a: 1})
})
// Draw connected bodies
draw2d.circle(bodyA.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1})
draw2d.circle(bodyB.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1})
draw2d.line([bodyA.position, bodyB.position], {r: 1, g: 1, b: 0, a: 0.5})
// Draw UI
draw2d.text("Box2D Demo", {x: 10, y: 10}, 20, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("Controls:", {x: 10, y: 40}, 16, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- WASD: Move camera", {x: 10, y: 60}, 14, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- Q/E: Zoom in/out", {x: 10, y: 80}, 14, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- Left click: Create box", {x: 10, y: 100}, 14, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- Shift + Left click: Create circle", {x: 10, y: 120}, 14, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- Right click: Apply impulse", {x: 10, y: 140}, 14, {r: 1, g: 1, b: 1, a: 1})
draw2d.text("- R: Reset bodies", {x: 10, y: 160}, 14, {r: 1, g: 1, b: 1, a: 1})
// Show physics stats
draw2d.text("Bodies: " + (boxes.length + circles.length + 4), {x: 10, y: 200}, 14, {r: 1, g: 1, b: 1, a: 1})
})
// Helper functions
function drawBox(pos, width, height, angle, color) {
draw2d.rectangle({x:pos.x,y:pos.y,width, height})
}
function screenToWorld(screenPos) {
return {
x: (screenPos.x - prosperon.x * 0.5) / camera.zoom + camera.x,
y: (screenPos.y - prosperon.y * 0.5) / camera.zoom + camera.y
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

9
examples/chess/config.js Normal file
View File

@@ -0,0 +1,9 @@
// Chess game configuration for Moth framework
return {
title: "Chess",
resolution: { width: 480, height: 480 },
internal_resolution: { width: 480, height: 480 },
fps: 60,
clearColor: [22/255, 120/255, 194/255, 1],
mode: 'stretch' // No letterboxing for chess
};

57
examples/chess/grid.js Normal file
View File

@@ -0,0 +1,57 @@
var CELLS = Symbol()
var key = function key(x,y) { return `${x},${y}` }
function grid(w, h)
{
this[CELLS] = new Map()
this.width = w;
this.height = h;
}
grid.prototype = {
cell(x,y) {
var k = key(x,y)
if (!this[CELLS].has(k)) this[CELLS].set(k,[])
return this[CELLS].get(k)
},
add(entity, pos) {
this.cell(pos.x, pos.y).push(entity);
entity.coord = pos.slice();
},
remove(entity, pos) {
var c = this.cell(pos.x, pos.y);
c.splice(c.indexOf(entity), 1);
},
at(pos) {
return this.cell(pos.x, pos.y);
},
inBounds(pos) {
return pos.x >= 0 && pos.x < this.width && pos.y >= 0 && pos.y < this.height;
},
each(fn) {
for (var [k, list] of this[CELLS])
for (var p of list) fn(p, p.coord);
},
toString() {
var out = `grid [${this.width}x${this.height}]
`
for (var y = 0; y < this.height; y++) {
for (var x = 0; x < this.width; x++) {
var cell = this.at([x,y]);
out += cell.length
}
if (y !== this.height - 1) out += "\n"
}
return out
},
}
return grid

403
examples/chess/main.js Normal file
View File

@@ -0,0 +1,403 @@
/* main.js runs the demo with your prototype-based grid */
var moth = use('moth', $_.delay)
var json = use('json')
var res = 160
var internal_res = 480
moth.initialize({ width:res, height:res, resolution_x:internal_res, resolution_y:internal_res, mode:'letterbox' });
var os = use('os');
var draw2d = use('draw2d');
var gfx = use('graphics');
/*──── import our pieces + systems ───────────────────────────────────*/
var Grid = use('grid'); // your new ctor
var MovementSystem = use('movement').MovementSystem;
var startingPos = use('pieces').startingPosition;
var rules = use('rules');
/*──── build board ───────────────────────────────────────────────────*/
var grid = new Grid(8, 8);
grid.width = 8; // (the ctor didn't store them)
grid.height = 8;
var mover = new MovementSystem(grid, rules);
startingPos(grid);
/*──── networking and game state ─────────────────────────────────────*/
var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected'
var isServer = false;
var opponent = null;
var myColor = null; // 'white' or 'black'
var isMyTurn = false;
function updateTitle() {
var title = "Misty Chess - ";
switch(gameState) {
case 'waiting':
title += "Press S to start server or J to join";
break;
case 'searching':
title += "Searching for server...";
break;
case 'server_waiting':
title += "Waiting for player to join...";
break;
case 'connected':
if (myColor) {
title += (mover.turn === myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
} else {
title += mover.turn + " turn";
}
break;
}
prosperon.window.title = title
}
// Initialize title
updateTitle();
/*──── mouse → click-to-move ─────────────────────────────────────────*/
var selectPos = null;
var hoverPos = null;
var holdingPiece = false;
var opponentMousePos = null;
var opponentHoldingPiece = false;
var opponentSelectPos = null;
prosperon.on('mouse_button_down', function(e) {
if (e.which !== 0) return;
// Don't allow piece selection unless we have an opponent
if (gameState !== 'connected' || !opponent) return;
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
if (!grid.inBounds(c)) return;
var cell = grid.at(c);
if (cell.length && cell[0].colour === mover.turn) {
selectPos = c;
holdingPiece = true;
// Send pickup notification to opponent
if (opponent) {
send(opponent, {
type: 'piece_pickup',
pos: c
});
}
} else {
selectPos = null;
}
})
prosperon.on('mouse_button_up', function(e) {
if (e.which !== 0 || !holdingPiece || !selectPos) return;
// Don't allow moves unless we have an opponent and it's our turn
if (gameState !== 'connected' || !opponent || !isMyTurn) {
holdingPiece = false;
return;
}
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
if (!grid.inBounds(c)) {
holdingPiece = false;
return;
}
if (mover.tryMove(grid.at(selectPos)[0], c)) {
console.log("Made move from", selectPos, "to", c);
// Send move to opponent
console.log("Sending move to opponent:", opponent);
send(opponent, {
type: 'move',
from: selectPos,
to: c
});
isMyTurn = false; // It's now opponent's turn
console.log("Move sent, now opponent's turn");
selectPos = null;
updateTitle();
}
holdingPiece = false;
// Send piece drop notification to opponent
if (opponent) {
send(opponent, {
type: 'piece_drop'
});
}
})
prosperon.on('mouse_motion', function(e) {
var mx = e.pos.x;
var my = e.pos.y;
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
if (!grid.inBounds(c)) {
hoverPos = null;
return;
}
hoverPos = c;
// Send mouse position to opponent in real-time
if (opponent && gameState === 'connected') {
send(opponent, {
type: 'mouse_move',
pos: c,
holding: holdingPiece,
selectPos: selectPos
});
}
})
/*──── drawing helpers ───────────────────────────────────────────────*/
/* ── constants ─────────────────────────────────────────────────── */
var S = 60; // square size in px
var light = [0.93,0.93,0.93,1];
var dark = [0.25,0.25,0.25,1];
var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves
var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse
var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse
/* ── draw one 8×8 chess board ──────────────────────────────────── */
function drawBoard() {
for (var y = 0; y < 8; ++y)
for (var x = 0; x < 8; ++x) {
var isMyHover = hoverPos && hoverPos[0] === x && hoverPos[1] === y;
var isOpponentHover = opponentMousePos && opponentMousePos[0] === x && opponentMousePos[1] === y;
var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]);
var color = ((x+y)&1) ? dark : light;
if (isValidMove) {
color = allowedColor; // Gold for allowed moves
} else if (isMyHover && !isOpponentHover) {
color = myMouseColor; // Green for my mouse
} else if (isOpponentHover) {
color = opponentMouseColor; // Red for opponent mouse
}
draw2d.rectangle(
{ x: x*S, y: y*S, width: S, height: S },
{ thickness: 0, color: color }
);
}
}
function isValidMoveForTurn(from, to) {
if (!grid.inBounds(to)) return false;
var piece = grid.at(from)[0];
if (!piece) return false;
// Check if the destination has a piece of the same color
var destCell = grid.at(to);
if (destCell.length && destCell[0].colour === piece.colour) {
return false;
}
return rules.canMove(piece, from, to, grid);
}
/* ── draw every live piece ─────────────────────────────────────── */
function drawPieces() {
grid.each(function (piece) {
if (piece.captured) return;
// Skip drawing the piece being held (by me or opponent)
if (holdingPiece && selectPos &&
piece.coord[0] === selectPos[0] &&
piece.coord[1] === selectPos[1]) {
return;
}
// Skip drawing the piece being held by opponent
if (opponentHoldingPiece && opponentSelectPos &&
piece.coord[0] === opponentSelectPos[0] &&
piece.coord[1] === opponentSelectPos[1]) {
return;
}
var r = { x: piece.coord[0]*S, y: piece.coord[1]*S,
width:S, height:S };
draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"});
});
// Draw the held piece at the mouse position if we're holding one
if (holdingPiece && selectPos && hoverPos) {
var piece = grid.at(selectPos)[0];
if (piece) {
var r = { x: hoverPos[0]*S, y: hoverPos[1]*S,
width:S, height:S };
draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"});
}
}
// Draw opponent's held piece if they're dragging one
if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) {
var opponentPiece = grid.at(opponentSelectPos)[0];
if (opponentPiece) {
var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S,
width:S, height:S };
// Draw with slight transparency to show it's the opponent's piece
draw2d.image(opponentPiece.sprite, r, 0, [0,0], [0,0], {mode:"nearest", color: [1, 1, 1, 0.7]});
}
}
}
var graphics = use('graphics')
prosperon.on('draw', function() {
drawBoard()
drawPieces()
draw2d.text("HELL", [100,100])
})
prosperon.on('key_down', function(e) {
// S key - start server
if (e.scancode === 22 && gameState === 'waiting') { // S key
startServer();
}
// J key - join server
else if (e.scancode === 13 && gameState === 'waiting') { // J key
joinServer();
}
})
function startServer() {
gameState = 'server_waiting';
isServer = true;
myColor = 'white';
isMyTurn = true;
updateTitle();
$_.portal(e => {
console.log("Portal received contact message");
// Reply with this actor to establish connection
console.log (json.encode($_))
send(e, $_);
console.log("Portal replied with server actor");
}, 5678);
}
function joinServer() {
gameState = 'searching';
updateTitle();
function contact_fn(actor, reason) {
console.log("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason);
if (actor) {
opponent = actor;
console.log("Connection established with server, sending join request");
// Send a greet message with our actor object
send(opponent, {
type: 'greet',
client_actor: $_
});
} else {
console.log(`Failed to connect: ${json.encode(reason)}`);
gameState = 'waiting';
updateTitle();
}
}
$_.contact(contact_fn, {
address: "192.168.0.149",
port: 5678
});
}
var os = use('os')
var actor = use('actor')
for (var i in actor) console.log(i)
// Set up IO actor subscription
var ioguy = {
__ACTORDATA__: {
id: actor.ioactor()
}
};
send(ioguy, {
type: "subscribe",
actor: $_
});
$_.receiver(e => {
if (e.type === 'game_start' || e.type === 'move' || e.type === 'greet')
console.log("Receiver got message:", e.type, e);
if (e.type === 'quit') os.exit()
if (e.type === 'greet') {
console.log("Server received greet from client");
// Store the client's actor object for ongoing communication
opponent = e.client_actor;
console.log("Stored client actor:", json.encode(opponent));
gameState = 'connected';
updateTitle();
// Send game_start to the client
console.log("Sending game_start to client");
send(opponent, {
type: 'game_start',
your_color: 'black'
});
console.log("game_start message sent to client");
}
else if (e.type === 'game_start') {
console.log("Game starting, I am:", e.your_color);
myColor = e.your_color;
isMyTurn = (myColor === 'white');
gameState = 'connected';
updateTitle();
} else if (e.type === 'move') {
console.log("Received move from opponent:", e.from, "to", e.to);
// Apply opponent's move
var fromCell = grid.at(e.from);
if (fromCell.length) {
var piece = fromCell[0];
if (mover.tryMove(piece, e.to)) {
isMyTurn = true; // It's now our turn
updateTitle();
console.log("Applied opponent move, now my turn");
} else {
console.log("Failed to apply opponent move");
}
} else {
console.log("No piece found at from position");
}
} else if (e.type === 'mouse_move') {
// Update opponent's mouse position
opponentMousePos = e.pos;
opponentHoldingPiece = e.holding;
opponentSelectPos = e.selectPos;
} else if (e.type === 'piece_pickup') {
// Opponent picked up a piece
opponentSelectPos = e.pos;
opponentHoldingPiece = true;
} else if (e.type === 'piece_drop') {
// Opponent dropped their piece
opponentHoldingPiece = false;
opponentSelectPos = null;
}
prosperon.dispatch(e.type, e)
})

View File

@@ -0,0 +1,32 @@
var MovementSystem = function(grid, rules) {
this.grid = grid;
this.rules = rules || {}; // expects { canMove: fn }
this.turn = 'white';
}
MovementSystem.prototype.tryMove = function (piece, to) {
if (piece.colour !== this.turn) return false;
// normalise to into our hybrid coord
var dest = [to.x !== undefined ? to.x : to[0],
to.y !== undefined ? to.y : to[1]];
if (!this.grid.inBounds(dest)) return false;
if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false;
var victims = this.grid.at(dest);
if (victims.length && victims[0].colour === piece.colour) return false;
if (victims.length) victims[0].captured = true;
this.grid.remove(piece, piece.coord);
this.grid.add (piece, dest);
// grid.add() re-creates coord; re-add .x/.y fields:
piece.coord.x = dest.x;
piece.coord.y = dest.y;
this.turn = (this.turn === 'white') ? 'black' : 'white';
return true;
};
return { MovementSystem: MovementSystem };

29
examples/chess/pieces.js Normal file
View File

@@ -0,0 +1,29 @@
/* pieces.js simple data holders + starting layout */
function Piece(kind, colour) {
this.kind = kind; // "pawn" etc.
this.colour = colour; // "white"/"black"
this.sprite = colour + '_' + kind; // for draw2d.image
this.captured = false;
this.coord = [0,0];
}
Piece.prototype.toString = function () {
return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase();
};
function startingPosition(grid) {
var W = 'white', B = 'black', x;
// pawns
for (x = 0; x < 8; x++) {
grid.add(new Piece('pawn', W), [x, 6]);
grid.add(new Piece('pawn', B), [x, 1]);
}
// major pieces
var back = ['rook','knight','bishop','queen','king','bishop','knight','rook'];
for (x = 0; x < 8; x++) {
grid.add(new Piece(back[x], W), [x, 7]);
grid.add(new Piece(back[x], B), [x, 0]);
}
}
return { Piece, startingPosition };

BIN
examples/chess/prosperon Executable file

Binary file not shown.

45
examples/chess/rules.js Normal file
View File

@@ -0,0 +1,45 @@
/* helper robust coord access */
function cx(c) { return (c.x !== undefined) ? c.x : c[0]; }
function cy(c) { return (c.y !== undefined) ? c.y : c[1]; }
/* simple move-shape checks */
var deltas = {
pawn: function (pc, dx, dy, grid, to) {
var dir = (pc.colour === 'white') ? -1 : 1;
var base = (pc.colour === 'white') ? 6 : 1;
var one = (dy === dir && dx === 0 && grid.at(to).length === 0);
var two = (dy === 2 * dir && dx === 0 && cy(pc.coord) === base &&
grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length === 0 &&
grid.at(to).length === 0);
var cap = (dy === dir && Math.abs(dx) === 1 && grid.at(to).length);
return one || two || cap;
},
rook : function (pc, dx, dy) { return (dx === 0 || dy === 0); },
bishop: function (pc, dx, dy) { return Math.abs(dx) === Math.abs(dy); },
queen : function (pc, dx, dy) { return (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)); },
knight: function (pc, dx, dy) { return (Math.abs(dx) === 1 && Math.abs(dy) === 2) ||
(Math.abs(dx) === 2 && Math.abs(dy) === 1); },
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) === 1; }
};
function clearLine(from, to, grid) {
var dx = Math.sign(cx(to) - cx(from));
var dy = Math.sign(cy(to) - cy(from));
var x = cx(from) + dx, y = cy(from) + dy;
while (x !== cx(to) || y !== cy(to)) {
if (grid.at({ x: x, y: y }).length) return false;
x += dx; y += dy;
}
return true;
}
function canMove(piece, from, to, grid) {
var dx = cx(to) - cx(from);
var dy = cy(to) - cy(from);
var f = deltas[piece.kind];
if (!f || !f(piece, dx, dy, grid, to)) return false;
if (piece.kind === 'knight') return true;
return clearLine(from, to, grid);
}
return { canMove };

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,234 @@
// HTTP Download Actor
// Handles download requests and progress queries
var http = use('http');
var os = use('os');
// Actor state
var state = {
downloading: false,
current_url: null,
total_bytes: 0,
downloaded_bytes: 0,
start_time: 0,
error: null,
connection: null,
download_msg: null,
chunks: []
};
// Helper to calculate progress percentage
function get_progress() {
if (state.total_bytes === 0) {
return 0;
}
return Math.round((state.downloaded_bytes / state.total_bytes) * 100);
}
// Helper to format status response
function get_status() {
if (!state.downloading) {
return {
status: 'idle',
error: state.error
};
}
var elapsed = os.now() - state.start_time;
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
return {
status: 'downloading',
url: state.current_url,
progress: get_progress(),
downloaded_bytes: state.downloaded_bytes,
total_bytes: state.total_bytes,
elapsed_seconds: elapsed,
bytes_per_second: Math.round(bytes_per_sec)
};
}
// Main message receiver
$_.receiver(function(msg) {
switch (msg.type) {
case 'download':
if (state.downloading) {
send(msg, {
type: 'error',
error: 'Already downloading',
current_url: state.current_url
});
return;
}
if (!msg.url) {
send(msg, {
type: 'error',
error: 'No URL provided'
});
return;
}
// Start download
state.downloading = true;
state.current_url = msg.url;
state.total_bytes = 0;
state.downloaded_bytes = 0;
state.start_time = os.now();
state.error = null;
state.download_msg = msg;
state.chunks = [];
try {
// Start the connection
state.connection = http.fetch_start(msg.url, msg.options || {});
if (!state.connection) {
throw new Error('Failed to start download');
}
// Schedule the first chunk read
$_.delay(read_next_chunk, 0);
} catch (e) {
state.error = e.toString();
state.downloading = false;
send(msg, {
type: 'error',
error: state.error,
url: msg.url
});
}
break;
case 'status':
console.log(`got status request. current is ${json.encode(get_status())}`)
send(msg, {
type: 'status_response',
...get_status()
});
break;
case 'cancel':
if (state.downloading) {
// Cancel the download
if (state.connection) {
http.fetch_close(state.connection);
state.connection = null;
}
state.downloading = false;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
send(msg, {
type: 'cancelled',
message: 'Download cancelled',
url: state.current_url
});
} else {
send(msg, {
type: 'error',
error: 'No download in progress'
});
}
break;
default:
send(msg, {
type: 'error',
error: 'Unknown message type: ' + msg.type
});
}
});
// Non-blocking chunk reader
function read_next_chunk() {
if (!state.downloading || !state.connection) {
return;
}
try {
var chunk = http.fetch_read_chunk(state.connection);
if (chunk === null) {
// Download complete
finish_download();
return;
}
// Store chunk
state.chunks.push(chunk);
// Update progress
var info = http.fetch_info(state.connection);
state.downloaded_bytes = info.bytes_read;
if (info.headers_complete && info.content_length > 0) {
state.total_bytes = info.content_length;
}
// Schedule next chunk read
$_.delay(read_next_chunk, 0);
} catch (e) {
// Error during download
state.error = e.toString();
if (state.connection) {
http.fetch_close(state.connection);
}
if (state.download_msg) {
send(state.download_msg, {
type: 'error',
error: state.error,
url: state.current_url
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.download_msg = null;
state.chunks = [];
}
}
// Complete the download and send result
function finish_download() {
if (state.connection) {
http.fetch_close(state.connection);
}
// Combine all chunks into single ArrayBuffer
var total_size = 0;
for (var i = 0; i < state.chunks.length; i++) {
total_size += state.chunks[i].byteLength;
}
var result = new ArrayBuffer(total_size);
var view = new Uint8Array(result);
var offset = 0;
for (var i = 0; i < state.chunks.length; i++) {
var chunk_view = new Uint8Array(state.chunks[i]);
view.set(chunk_view, offset);
offset += state.chunks[i].byteLength;
}
// Send complete message
if (state.download_msg) {
send(state.download_msg, {
type: 'complete',
url: state.current_url,
data: result,
size: result.byteLength,
duration: os.now() - state.start_time
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
}

29
examples/nat.js Normal file
View File

@@ -0,0 +1,29 @@
// NAT Punchthrough Server
// This server helps two chess clients find each other through NAT
// The server coordinates the punchthrough by having both clients
// connect to each other simultaneously
var json = use('json');
var waiting_client = null;
var match_id = 0;
$_.portal(e => {
console.log("NAT server: received connection request");
if (!is_actor(e.actor))
send(e, {reason: "Must provide the actor you want to connect."});
if (waiting_client) {
console.log(`sending out messages! to ${json.encode(e.actor)} and ${json.encode(waiting_client.actor)}`)
send(waiting_client, e.actor)
send(e, waiting_client.actor)
waiting_client = undefined
return
}
waiting_client = e
console.log(`actor ${json.encode(e.actor)} is waiting ...`)
}, 4000);

22
examples/nat_client.js Normal file
View File

@@ -0,0 +1,22 @@
console.log(`nat client starting`)
$_.contact((actor, reason) => {
if (actor) {
console.log(`trying to message ${json.encode(actor)}`)
send(actor, {type:"greet"})
} else {
console.log(json.encode(reason))
}
}, {
address: "108.210.60.32", // NAT server's public IP
port: 4000,
actor: $_
})
$_.receiver(e => {
switch(e.type) {
case 'greet':
console.log(`hello!`)
break
}
})

187
examples/steam_example.js Normal file
View File

@@ -0,0 +1,187 @@
// Steam Integration Example
// This example shows how to use Steam achievements and stats
var steam = use("steam");
// Achievement names (these should match your Steam app configuration)
var ACHIEVEMENTS = {
FIRST_WIN: "ACH_FIRST_WIN",
PLAY_10_GAMES: "ACH_PLAY_10_GAMES",
HIGH_SCORE: "ACH_HIGH_SCORE_1000"
};
// Stat names
var STATS = {
GAMES_PLAYED: "stat_games_played",
TOTAL_SCORE: "stat_total_score",
PLAY_TIME: "stat_play_time"
};
var steam_available = false;
var stats_loaded = false;
// Initialize Steam
function init_steam() {
if (!steam) {
console.log("Steam module not available");
return false;
}
console.log("Initializing Steam...");
steam_available = steam.steam_init();
if (steam_available) {
console.log("Steam initialized successfully");
// Request current stats/achievements
if (steam.stats.stats_request()) {
console.log("Stats requested");
stats_loaded = true;
}
} else {
console.log("Failed to initialize Steam");
}
return steam_available;
}
// Update Steam (call this regularly, e.g., once per frame)
function update_steam() {
if (steam_available) {
steam.steam_run_callbacks();
}
}
// Unlock an achievement
function unlock_achievement(achievement_name) {
if (!steam_available || !stats_loaded) return false;
// Check if already unlocked
var unlocked = steam.achievement.achievement_get(achievement_name);
if (unlocked) {
console.log("Achievement already unlocked:", achievement_name);
return true;
}
// Unlock it
if (steam.achievement.achievement_set(achievement_name)) {
console.log("Achievement unlocked:", achievement_name);
// Store stats to make it permanent
steam.stats.stats_store();
return true;
}
return false;
}
// Update a stat
function update_stat(stat_name, value, is_float) {
if (!steam_available || !stats_loaded) return false;
var success;
if (is_float) {
success = steam.stats.stats_set_float(stat_name, value);
} else {
success = steam.stats.stats_set_int(stat_name, value);
}
if (success) {
console.log("Stat updated:", stat_name, "=", value);
steam.stats.stats_store();
}
return success;
}
// Get a stat value
function get_stat(stat_name, is_float) {
if (!steam_available || !stats_loaded) return 0;
if (is_float) {
return steam.stats.stats_get_float(stat_name) || 0;
} else {
return steam.stats.stats_get_int(stat_name) || 0;
}
}
// Example game logic
var games_played = 0;
var total_score = 0;
var current_score = 0;
function start_game() {
games_played = get_stat(STATS.GAMES_PLAYED, false);
total_score = get_stat(STATS.TOTAL_SCORE, false);
current_score = 0;
console.log("Starting game #" + (games_played + 1));
}
function end_game(score) {
current_score = score;
games_played++;
total_score += score;
// Update stats
update_stat(STATS.GAMES_PLAYED, games_played, false);
update_stat(STATS.TOTAL_SCORE, total_score, false);
// Check for achievements
if (games_played === 1) {
unlock_achievement(ACHIEVEMENTS.FIRST_WIN);
}
if (games_played >= 10) {
unlock_achievement(ACHIEVEMENTS.PLAY_10_GAMES);
}
if (score >= 1000) {
unlock_achievement(ACHIEVEMENTS.HIGH_SCORE);
}
}
// Cloud save example
function save_to_cloud(save_data) {
if (!steam_available) return false;
var json_data = JSON.stringify(save_data);
return steam.cloud.cloud_write("savegame.json", json_data);
}
function load_from_cloud() {
if (!steam_available) return null;
var data = steam.cloud.cloud_read("savegame.json");
if (data) {
// Convert ArrayBuffer to string
var decoder = new TextDecoder();
var json_str = decoder.decode(data);
return JSON.parse(json_str);
}
return null;
}
// Cleanup
function cleanup_steam() {
if (steam_available) {
steam.steam_shutdown();
console.log("Steam shut down");
}
}
// Export the API
module.exports = {
init: init_steam,
update: update_steam,
cleanup: cleanup_steam,
unlock_achievement: unlock_achievement,
update_stat: update_stat,
get_stat: get_stat,
start_game: start_game,
end_game: end_game,
save_to_cloud: save_to_cloud,
load_from_cloud: load_from_cloud,
is_available: function() { return steam_available; }
};

View File

@@ -1,13 +1,16 @@
project('prosperon', ['c', 'cpp'], default_options : [ 'cpp_std=c++11']) project('prosperon', ['c', 'cpp'],
version: '0.9.3',
meson_version: '>=1.4',
default_options : [ 'cpp_std=c++11'])
libtype = get_option('default_library') libtype = get_option('default_library')
link = [] link = []
src = [] src = []
if not get_option('editor') fs = import('fs')
add_project_arguments('-DNEDITOR', language:'c')
endif add_project_arguments('-pedantic', language: ['c'])
git_tag_cmd = run_command('git', 'describe', '--tags', '--abbrev=0', check: false) git_tag_cmd = run_command('git', 'describe', '--tags', '--abbrev=0', check: false)
prosperon_version = 'unknown' prosperon_version = 'unknown'
@@ -31,21 +34,79 @@ add_project_arguments(
add_project_arguments('-Wno-incompatible-pointer-types', language: 'c') add_project_arguments('-Wno-incompatible-pointer-types', language: 'c')
add_project_arguments('-Wno-narrowing', language: 'cpp') add_project_arguments('-Wno-narrowing', language: 'cpp')
add_project_arguments('-Wno-missing-braces', language:'c') add_project_arguments('-Wno-missing-braces', language:'c')
add_project_arguments('-Wl,--disable-new-dtags', language:'cpp') add_project_arguments('-Wno-strict-prototypes', language:'c')
add_project_arguments('-Wl,--disable-new-dtags', language:'c') add_project_arguments('-Wno-unused-command-line-argument', language: 'c')
add_project_arguments('-Wno-unused-command-line-argument', language: 'cpp')
deps = [] deps = []
if host_machine.system() == 'darwin' if host_machine.system() == 'darwin'
add_project_arguments('-x', 'objective-c', language: 'c') add_project_arguments('-x', 'objective-c', language: 'c')
fworks = ['foundation', 'metal', 'audiotoolbox', 'metalkit', 'avfoundation', 'quartzcore', 'cocoa'] fworks = [
'foundation',
'metal',
'audiotoolbox',
'metalkit',
'avfoundation',
'quartzcore',
'cocoa',
'coreaudio',
'coremedia',
'gamecontroller',
'forcefeedback',
'iokit',
'corefoundation',
'corehaptics',
'carbon',
'uniformtypeidentifiers'
]
foreach fkit : fworks foreach fkit : fworks
deps += dependency('appleframeworks', modules: fkit) deps += dependency('appleframeworks', modules: fkit)
endforeach endforeach
endif endif
cmake = import('cmake')
mbedtls_opts = cmake.subproject_options()
mbedtls_opts.add_cmake_defines({
'ENABLE_PROGRAMS': 'OFF', # Disable Mbed TLS programs
'ENABLE_TESTING': 'OFF', # Disable Mbed TLS tests
'CMAKE_BUILD_TYPE': 'Release', # Optimize for release
'MBEDTLS_FATAL_WARNINGS': 'ON', # Treat warnings as errors
'USE_STATIC_MBEDTLS_LIBRARY': 'ON',# Build static libraries
'USE_SHARED_MBEDTLS_LIBRARY': 'OFF'# Disable shared libraries
})
mbedtls_proj = cmake.subproject('mbedtls', options: mbedtls_opts)
deps += [
mbedtls_proj.dependency('mbedtls'),
mbedtls_proj.dependency('mbedx509'),
mbedtls_proj.dependency('mbedcrypto')
]
sdl3_opts = cmake.subproject_options()
sdl3_opts.add_cmake_defines({
'SDL_STATIC': 'ON',
'SDL_SHARED': 'OFF',
'SDL_TEST': 'OFF',
'CMAKE_BUILD_TYPE': 'Release',
'SDL_THREADS': 'ON',
'SDL_PIPEWIRE': 'ON',
'SDL_PULSEAUDIO': 'ON',
})
box2d_opts = cmake.subproject_options()
box2d_opts.add_cmake_defines({
'BOX2D_SAMPLES': 'OFF',
'BOX2D_BUILD_STATIC': 'ON',
'BOX2d_AVX2': 'ON',
'BOX2D_BUILD_SHARED': 'OFF',
'CMAKE_BUILD_TYPE': 'Release',
})
box2d_proj = cmake.subproject('box2d', options: box2d_opts)
deps += box2d_proj.dependency('box2d')
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
deps += dependency('sdl3', required:true)
if host_machine.system() == 'darwin' if host_machine.system() == 'darwin'
deps += dependency('appleframeworks', modules: 'accelerate') deps += dependency('appleframeworks', modules: 'accelerate')
@@ -56,81 +117,130 @@ endif
if host_machine.system() == 'linux' if host_machine.system() == 'linux'
deps += cc.find_library('asound', required:true) deps += cc.find_library('asound', required:true)
deps += [dependency('x11'), dependency('xi'), dependency('xcursor'), dependency('egl'), dependency('gl')] deps += [dependency('x11'), dependency('xi'), dependency('xcursor'), dependency('egl'), dependency('gl')]
# link += '-fuse-ld=mold' # use mold, which is very fast, for debug builds
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
deps += cc.find_library('d3d11') deps += cc.find_library('d3d11')
# these are for tracy
deps += cc.find_library('ws2_32', required:true) deps += cc.find_library('ws2_32', required:true)
deps += cc.find_library('dbghelp') deps += cc.find_library('dbghelp')
#end deps += cc.find_library('winmm')
link += '-static' # Required to pack in mingw dlls on cross compilation deps += cc.find_library('setupapi')
deps += cc.find_library('imm32')
deps += cc.find_library('version')
deps += cc.find_library('cfgmgr32')
deps += cc.find_library('bcrypt')
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # Hack for MSYS2
sdl3_opts.add_cmake_defines({'HAVE_ISNAN': '1'})
link += '-static'
endif endif
if host_machine.system() == 'emscripten' if host_machine.system() == 'emscripten'
link += '-sUSE_WEBGPU' link += '-sUSE_WEBGPU'
# Use the pre-installed copy
deps += dependency('sdl3',
static : true,
method : 'pkg-config', # or 'cmake' if you prefer
required : true)
else
sdl3_proj = cmake.subproject('sdl3', options : sdl3_opts)
deps += sdl3_proj.dependency('SDL3-static')
endif endif
tracy_opts = ['fibers=true', 'on_demand=true']
quickjs_opts = [] quickjs_opts = []
src += 'qjs_tracy.c'
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
deps += dependency('tracy', static:true, default_options:tracy_opts)
quickjs_opts += 'default_library=static' quickjs_opts += 'default_library=static'
deps += dependency('quickjs', static:true, default_options:quickjs_opts) deps += dependency('quickjs', static:true, default_options:quickjs_opts)
deps += dependency('qjs-layout', static:true)
storefront = get_option('storefront') deps += dependency('qjs-miniz', static:true)
if storefront == 'steam'
deps += dependency('qjs-steam',static:false)
endif
deps += dependency('qjs-layout',static:true)
deps += dependency('qjs-nota',static:true)
deps += dependency('qjs-miniz',static:true)
deps += dependency('qjs-soloud',static:true)
deps += dependency('physfs', static:true) deps += dependency('physfs', static:true)
#deps += dependency('opencv4')
#deps += cc.find_library('opencv')
deps += dependency('threads') deps += dependency('threads')
deps += dependency('chipmunk') if host_machine.system() != 'emscripten'
deps += dependency('enet', static:true)
src += 'qjs_enet.c'
if get_option('chipmunk') src += 'qjs_tracy.c'
# deps += dependency('qjs-chipmunk', static:false) tracy_opts = ['fibers=true', 'on_demand=true']
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
deps += dependency('tracy', static:true, default_options:tracy_opts)
src += 'qjs_dmon.c'
endif endif
if get_option('enet') deps += dependency('soloud', static:true)
#deps += dependency('qjs-enet', static:false) deps += dependency('libqrencode', static:true)
# Storefront SDK support
storefront = get_option('storefront')
if storefront == 'steam'
steam_sdk_path = meson.current_source_dir() / 'sdk'
if host_machine.system() == 'darwin'
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'osx' / 'libsteam_api.dylib'
elif host_machine.system() == 'linux'
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'linux64' / 'libsteam_api.so'
elif host_machine.system() == 'windows'
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'win64' / 'steam_api64.lib'
else
steam_lib_path = ''
endif
if fs.exists(steam_lib_path)
steam_dep = declare_dependency(
include_directories: include_directories('sdk/public'),
link_args: [steam_lib_path]
)
deps += steam_dep
src += 'qjs_steam.cpp'
message('Steam SDK enabled')
else
error('Steam SDK required but not found at: ' + steam_lib_path)
endif
else
add_project_arguments('-DNSTEAM', language: ['c', 'cpp'])
message('Storefront: ' + storefront)
endif endif
link_args = link
sources = [] sources = []
src += ['anim.c', 'config.c', 'datastream.c','font.c','gameobject.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'quadtree.c', 'aabb.c', 'rtree.c']
imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp'] src += [
'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c',
'render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c',
'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c', 'qjs_sdl.c', 'qjs_math.c', 'qjs_geometry.c', 'qjs_transform.c', 'qjs_sprite.c', 'qjs_io.c', 'qjs_os.c', 'qjs_actor.c',
'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c'
]
src += 'qjs_box2d.c'
# quirc src
src += [
'thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c',
'thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'
]
imsrc = [
'GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp',
'imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp',
'implot_items.cpp','implot.cpp','imgui_impl_sdlrenderer3.cpp','imgui_impl_sdl3.cpp',
'imgui_impl_sdlgpu3.cpp'
]
srceng = 'source' srceng = 'source'
tp = srceng / 'thirdparty' tp = srceng / 'thirdparty'
includes = [
includes = [srceng,tp / 'cgltf',tp / 'imgui',tp / 'par',tp / 'stb',tp,tp / 'pl_mpeg/include'] srceng, tp / 'cgltf', tp / 'imgui', tp / 'par', tp / 'stb',
tp, tp / 'pl_mpeg/include', tp / 'quirc'
]
foreach file : src foreach file : src
full_path = join_paths('source', file) full_path = join_paths('source', file)
sources += files(full_path) sources += files(full_path)
endforeach endforeach
if get_option('editor') if get_option('editor')
sources += 'source/qjs_imgui.cpp' # sources += 'source/qjs_imgui.cpp'
foreach imgui : imsrc # foreach imgui : imsrc
sources += tp / 'imgui' / imgui # sources += tp / 'imgui' / imgui
endforeach # endforeach
# sub_dmon = subproject('qjs-dmon')
# dmon_dep = sub_dmon.get_variable('qjs_dmon_dep')
endif endif
includers = [] includers = []
@@ -144,7 +254,7 @@ foreach folder: zip_folders
zip_paths += meson.project_source_root() / folder zip_paths += meson.project_source_root() / folder
endforeach endforeach
# Now use the hash file as a dependency so that any change in the files causes a rebuild. # Produce core.zip
core = custom_target('core.zip', core = custom_target('core.zip',
output : 'core.zip', output : 'core.zip',
command : ['sh', '-c', command : ['sh', '-c',
@@ -152,7 +262,7 @@ core = custom_target('core.zip',
' && echo "Rebuilding core.zip" && rm -f ' + meson.current_build_dir() + '/core.zip && ' + ' && echo "Rebuilding core.zip" && rm -f ' + meson.current_build_dir() + '/core.zip && ' +
'zip -r ' + meson.current_build_dir() + '/core.zip scripts fonts icons shaders' 'zip -r ' + meson.current_build_dir() + '/core.zip scripts fonts icons shaders'
], ],
build_always: true, build_always_stale: true,
build_by_default: true build_by_default: true
) )
@@ -164,6 +274,26 @@ prosperon_raw = executable('prosperon_raw', sources,
install:false install:false
) )
strip_enabled = ['release', 'minsize'].contains(get_option('buildtype'))
if strip_enabled
prosperon_raw_stripped = custom_target('prosperon_raw_stripped',
input: prosperon_raw,
output: 'prosperon_raw_stripped',
command: [
'sh', '-c',
'strip "$1" && cp "$1" "$2"',
'stripper',
'@INPUT@',
'@OUTPUT@'
],
build_by_default: true
)
exe_for_concat = prosperon_raw_stripped
else
exe_for_concat = prosperon_raw
endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
exe_ext = '.exe' exe_ext = '.exe'
else else
@@ -172,7 +302,7 @@ endif
prosperon = custom_target('prosperon', prosperon = custom_target('prosperon',
output: 'prosperon' + exe_ext, output: 'prosperon' + exe_ext,
input: [prosperon_raw, core], input: [exe_for_concat, core],
command: [ command: [
'sh', '-c', 'sh', '-c',
'cat "$1" "$2" > "$3" && chmod +x "$3" >/dev/null 2>&1', 'cat "$1" "$2" > "$3" && chmod +x "$3" >/dev/null 2>&1',
@@ -181,12 +311,12 @@ prosperon = custom_target('prosperon',
'@INPUT1@', '@INPUT1@',
'@OUTPUT@' '@OUTPUT@'
], ],
build_always: true, build_always_stale: true,
build_by_default: true build_by_default: true
) )
prosperon_dep = declare_dependency( prosperon_dep = declare_dependency(
link_with:prosperon link_with: prosperon
) )
copy_tests = custom_target( copy_tests = custom_target(
@@ -194,19 +324,24 @@ copy_tests = custom_target(
output: 'tests', output: 'tests',
command: [ command: [
'cp', '-rf', 'cp', '-rf',
join_paths(meson.source_root(), 'tests'), join_paths(meson.project_source_root(), 'tests'),
meson.build_root() meson.project_build_root()
], ],
build_always: true, build_always_stale: true,
build_by_default: true build_by_default: true
) )
tests = [ tests = [
'spawn_actor', 'spawn_actor',
'empty' 'empty',
'nota',
'wota',
'portalspawner',
'overling',
'send',
'delay'
] ]
foreach file : tests foreach file : tests
test(file, prosperon_raw, args:['tests/' + file + '.js'], depends:copy_tests) test(file, prosperon_raw, args:['tests/' + file + '.js'], depends:copy_tests)
endforeach endforeach

View File

@@ -1,4 +1,3 @@
option('editor', type:'boolean', value:true) option('editor', type:'boolean', value:true)
option('chipmunk', type:'boolean', value:true) option('chipmunk', type:'boolean', value:true)
option('enet', type:'boolean', value:true)
option('storefront', type:'combo', choices:['none','steam', 'gog', 'egs'], value:'none') option('storefront', type:'combo', choices:['none','steam', 'gog', 'egs'], value:'none')

View File

@@ -1,9 +1,9 @@
site_name: Prosperon Documentation site_name: Prosperon Documentation
edit_uri: edit/master/doc/docs
plugins: plugins:
- search - search
- awesome-pages - awesome-pages
- mike
extra_css: extra_css:
- style.css - style.css
@@ -32,6 +32,9 @@ extra:
analytics: analytics:
provider: google provider: google
property: G-85ECSFGCBV property: G-85ECSFGCBV
version:
default: latest
provider: mike
markdown_extensions: markdown_extensions:
- admonition - admonition

View File

@@ -625,13 +625,6 @@ debug or serialization. Implementation details may vary.
:return: A quoted version of the string. :return: A quoted version of the string.
`; `;
(String.prototype.localeCompare)[prosperon.DOC] = `Return a number indicating whether this string is less than, equal to, or
greater than 'compareString' in sort order, according to the current locale.
:param compareString: The string to compare against.
:return: A negative number if less, 0 if the same, or a positive number if greater.
`;
(String.prototype.toLowerCase)[prosperon.DOC] = `Return a new string with all alphabetic characters converted to lowercase. (String.prototype.toLowerCase)[prosperon.DOC] = `Return a new string with all alphabetic characters converted to lowercase.
:return: The lowercase version of this string. :return: The lowercase version of this string.

1129
scripts/core/doc.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

164
scripts/core/io.js Normal file
View File

@@ -0,0 +1,164 @@
$_.unneeded(_ => {
}, Infinity)
var subscribers = []
var windows = []
var renderers = []
var os = use('os')
$_.receiver(e => {
if (e.type === "subscribe") {
if (!e.actor) throw Error('Got a subscribe message with no actor.');
subscribers.push(e.actor)
return;
}
if (e.type === "window") {
var window = windows[e.id]
switch (e.fn) {
case "create":
window = os.engine_start(e.config)
windows[e.id] = window
break;
case "fullscreen":
window.fullscreen()
break;
case "make_renderer":
var renderer = window.make_renderer(e.config || {})
renderers[e.renderer_id] = renderer
break;
case "keyboard_shown":
return window.keyboard_shown()
case "theme":
return window.theme()
case "safe_area":
return window.safe_area()
case "bordered":
window.bordered(e.value)
break;
case "set_icon":
window.set_icon(e.icon)
break;
case "set_title":
window.title = e.title
break;
case "get_title":
return window.title
case "set_size":
window.size = e.size
break;
case "get_size":
return window.size
case "mouse_grab":
window.mouse_grab(e.value)
break;
}
}
if (e.type === "render") {
var renderer = renderers[e.id]
switch (e.fn) {
case "draw_color":
renderer.draw_color(e.color)
break;
case "present":
renderer.present()
break;
case "clear":
renderer.clear()
break;
case "line":
renderer.line(e.config)
break;
case "point":
renderer.point(e.config)
break;
case "texture":
renderer.texture(e.texture, e.src_rect, e.dst_rect, e.angle, e.center)
break;
case "rects":
renderer.rects(e.rects)
break;
case "geometry":
renderer.geometry(e.vertices, e.indices)
break;
case "geometry2":
renderer.geometry2(e.vertices, e.indices)
break;
case "sprite":
renderer.sprite(e.config)
break;
case "load_texture":
renderer.load_texture(e.path)
break;
case "get_image":
renderer.get_image(e.config)
break;
case "scale":
renderer.scale(e.scale)
break;
case "logical_size":
renderer.logical_size(e.size)
break;
case "viewport":
renderer.viewport(e.viewport)
break;
case "clip":
renderer.clip(e.rect)
break;
case "vsync":
renderer.vsync(e.enable)
break;
case "coords":
renderer.coords(e.config)
break;
case "camera":
renderer.camera(e.cam, e.layer)
break;
case "screen2world":
return renderer.screen2world(e.point)
case "target":
renderer.target(e.target)
break;
}
}
for (var a of subscribers)
send(a, e);
});

View File

@@ -1,78 +0,0 @@
var ex = {}
ex[prosperon.DOC] = `
A set of utilities for iterating over a hierarchy of actor-like objects, as well
as managing tag-based lookups. Objects are assumed to have a "objects" property,
pointing to children or sub-objects, forming a tree.
`
function eachobj(obj, fn) {
var val = fn(obj)
if (val) return val
for (var o in obj.objects) {
if (obj.objects[o] === obj) console.error(`Object ${obj.toString()} is referenced by itself.`)
val = eachobj(obj.objects[o], fn)
if (val) return val
}
}
ex.all_objects = function (fn, startobj = world) {
return eachobj(startobj, fn)
}
ex.all_objects[prosperon.DOC] = `
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
:param startobj: The root object at which iteration begins, default is the global "world".
:return: The first truthy value returned by fn, or undefined if none.
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
`
ex.find_object = function (fn, startobj = world) {}
ex.find_object[prosperon.DOC] = `
:param fn: A callback or criteria to locate a particular object.
:param startobj: The root object at which search begins, default "world".
:return: Not yet implemented.
Intended to find a matching object within the hierarchy.
`
var gtags = {}
ex.tag_add = function (tag, obj) {
gtags[tag] ??= new Set()
gtags[tag].add(obj)
}
ex.tag_add[prosperon.DOC] = `
:param tag: A string tag to associate with the object.
:param obj: The object to add under this tag.
:return: None
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
`
ex.tag_rm = function (tag, obj) {
delete gtags[tag].delete(obj)
}
ex.tag_rm[prosperon.DOC] = `
:param tag: The tag to remove the object from.
:param obj: The object to remove from the tag set.
:return: None
Remove the given object from the specified tags set, if it exists.
`
ex.tag_clear_guid = function (obj) {
for (var tag in gtags) gtags[tag].delete(obj)
}
ex.tag_clear_guid[prosperon.DOC] = `
:param obj: The object whose tags should be cleared.
:return: None
Remove the object from all tag sets.
`
ex.objects_with_tag = function (tag) {
if (!gtags[tag]) return []
return Array.from(gtags[tag])
}
ex.objects_with_tag[prosperon.DOC] = `
:param tag: A string tag to look up.
:return: An array of objects associated with the given tag.
Retrieve all objects currently tagged with the specified tag.
`
return ex

26
scripts/modules/camera.js Normal file
View File

@@ -0,0 +1,26 @@
var camera = this
camera.list[prosperon.DOC] = `Return an array of available camera device IDs.
:return: An array of camera IDs, or undefined if no cameras are available.
`
camera.open[prosperon.DOC] = `Open a camera device with the given ID.
:param id: The camera ID to open.
:return: A camera object on success, or throws an error if the camera cannot be opened.
`
camera.name[prosperon.DOC] = `Return the name of the camera with the given ID.
:param id: The camera ID to query.
:return: A string with the camera's name, or throws an error if the name cannot be retrieved.
`
camera.position[prosperon.DOC] = `Return the physical position of the camera with the given ID.
:param id: The camera ID to query.
:return: A string indicating the camera position ("unknown", "front", or "back").
`
return this;

View File

@@ -1,7 +1,6 @@
var io = use('io') var io = use('io')
var util = use('util') var util = use('util')
var dumpfolder = ".prosperon"; var dumpfolder = ".prosperon";
io.mkdir(dumpfolder) io.mkdir(dumpfolder)
@@ -27,74 +26,52 @@ Cmdline.register_order = function (order, fn, doc, usage = "") {
Cmdline.register_order( Cmdline.register_order(
"makedoc", "makedoc",
function() { function() {
var doc = use('doc') var doc = use('doc')
var gs = ['console', 'prosperon', 'actor', 'use'] var gs = ['console', 'prosperon', 'actor', 'use']
Object.getOwnPropertyDescriptor(prosperon.c_types.transform, 'pos')[prosperon.DOC] = 'TEST DOC' for (var g of gs)
io.slurpwrite(`.src/docs/api/${g}.md`, doc.writeDocFile(globalThis[g], g))
console.log(Object.getOwnPropertyDescriptor(prosperon.c_types.transform,'pos')[prosperon.DOC]) var coredocs = io.enumerate("scripts/modules", 0)
coredocs = coredocs.filter(x => io.match("**/*.js", x)).map(x => x.name())
for (var g of gs) var APIPATH = '.src/docs/api/modules/'
io.slurpwrite(`.src/docs/api/${g}.md`, doc.writeDocFile(globalThis[g], g))
var coredocs = io.enumerate("scripts/modules", 0) for (var m of coredocs) {
coredocs = coredocs.filter(x => io.match("**/*.js", x)).map(x => x.name()) var u = use(m)
var path = `${APIPATH}${m}.md`
io.slurpwrite(path, doc.writeDocFile(u, m))
}
var TYPEPATH = '.src/docs/api/types/' var TYPEPATH = '.src/docs/api/types/'
for (var c in prosperon.c_types) { for (var c in prosperon.c_types)
io.slurpwrite(`${TYPEPATH}${c}.md`, doc.writeDocFile(prosperon.c_types[c], c)) io.slurpwrite(`${TYPEPATH}${c}.md`, doc.writeDocFile(prosperon.c_types[c], c))
}
var APIPATH = '.src/docs/api/modules/' var DULLPATH = '.src/docs/dull/'
var mixins = ['Object', 'String', 'Array', 'Map', 'WeakMap', 'Symbol','Set', 'WeakSet', 'ArrayBuffer', 'Function']
for (var m of mixins) {
var path = `${DULLPATH}${m}.md`
io.slurpwrite(path, doc.writeDocFile(globalThis[m].prototype, m))
}
for (var m of coredocs) { var dullgpath = '.src/docs/dull/globals/'
var u = use(m) var globals = ['Object', 'String', 'Array', 'Symbol', 'Number', 'Error','Function', 'Math']
var path = `${APIPATH}${m}.md` for (var m of globals) {
io.slurpwrite(path, doc.writeDocFile(u, m)) var path = `${dullgpath}${m}.md`
} io.slurpwrite(path, doc.writeDocFile(globalThis[m], m))
}
var DULLPATH = '.src/docs/dull/'
var mixins = ['Object', 'String', 'Array', 'Map', 'WeakMap', 'Symbol','Set', 'WeakSet', 'ArrayBuffer', 'Function']
for (var m of mixins) {
var path = `${DULLPATH}${m}.md`
io.slurpwrite(path, doc.writeDocFile(globalThis[m].prototype, m))
}
var dullgpath = '.src/docs/dull/globals/'
var globals = ['Object', 'String', 'Array', 'Symbol', 'Number', 'Error','Function', 'Math']
for (var m of globals) {
var path = `${dullgpath}${m}.md`
io.slurpwrite(path, doc.writeDocFile(globalThis[m], m))
}
"Make documentation." "Make documentation."
}) })
function spawn_root(script)
{
return actor.spawn(script, {}, function(underling, msg) {
if (msg.message !== "created") return;
Object.defineProperty(underling, 'then', {
configurable:false,
writable:false,
value:function() {
os.exit(0);
}
});
})
}
Cmdline.register_order( Cmdline.register_order(
"play", "play",
function (argv) { function (argv) {
var app var app
if (io.exists("main.js")) if (io.exists("main.js"))
app = spawn_root("main.js") app = actor.spawn("main.js")
else else
app = spawn_root("nogame.js") app = actor.spawn("nogame.js")
// rm actor so it can't be tampered
globalThis.actor = undefined
var loop = use('loop') var loop = use('loop')
while(1) loop.step(); while(1) loop.step();
@@ -141,28 +118,6 @@ Cmdline.register_order(
"OBJECT ?FILE?", "OBJECT ?FILE?",
); );
Cmdline.register_order(
"run",
function (script) {
var s = os.now()
script = script.join(" ");
if (!script) {
console.print("Need something to run.");
return;
}
try {
spawn_root(script)
} catch(e) {
console.error(e);
os.exit(1);
}
},
"Run a given script. SCRIPT can be the script itself, or a file containing the script",
"SCRIPT",
);
Cmdline.orders.script = Cmdline.orders.run;
Cmdline.print_order = function (fn) { Cmdline.print_order = function (fn) {
if (typeof fn === "string") fn = Cmdline.orders[fn]; if (typeof fn === "string") fn = Cmdline.orders[fn];
@@ -171,6 +126,50 @@ Cmdline.print_order = function (fn) {
console.print(fn.doc + "\n"); console.print(fn.doc + "\n");
}; };
function parse_args(argv)
{
var args = {};
for (var i = 0; i < argv.length; i++) {
if (argv[i].startsWith("--")) {
var key = argv[i].slice(2);
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
args[key] = argv[i + 1];
i++; // Skip the value
} else {
args[key] = true; // Flag without value
}
}
}
return args;
}
function unparse_args(args) {
var argv = [];
for (var key in args) {
if (args.hasOwnProperty(key)) {
argv.push("--" + key); // Add the flag with "--" prefix
if (args[key] !== true) {
argv.push(args[key]); // Add the value if it's not a boolean true flag
}
}
}
return argv;
}
Cmdline.register_order(
"spawn",
function(argv) {
prosperon.args = parse_args(argv)
if (!prosperon.args.cwd) prosperon.args.cwd = '.'
io.mount(prosperon.args.cwd)
if (!prosperon.args.program)
os.exit()
},
"Spawn a new prosperon actor.",
"TOPIC"
);
Cmdline.register_order( Cmdline.register_order(
"help", "help",
function (order) { function (order) {
@@ -199,21 +198,43 @@ Cmdline.register_order(
Cmdline.register_order( Cmdline.register_order(
"version", "version",
function () { function () {
console.print(`Prosperon version ${prosperon.version} [${prosperon.revision}]`); console.print(`Prosperon version ${prosperon.version} [${prosperon.revision}]` + "\n");
}, },
"Display Prosperon info.", "Display Prosperon info.",
); );
function cmd_args(cmds) { function cmd_args(cmds) {
cmds.shift() // Remove the leading 'prosperon'
if (cmds.length === 0) cmds[0] = "play"; cmds.shift();
else if (!Cmdline.orders[cmds[0]]) {
// assume it's a script // If there are no arguments left, assume we want to spawn main.js
cmds[1] = cmds[0] if (!cmds[0]) {
cmds[0] = "run" // => effectively do: prosperon spawn --program main.js
cmds.unshift("main.js");
cmds.unshift("--program");
cmds.unshift("spawn");
} else if (!Cmdline.orders[cmds[0]]) {
// If the first token isn't a recognized command, treat it as either
// a directory containing main.js, or a script to run directly.
var arg0 = cmds.shift();
if (io.is_directory(arg0)) {
var script = cmds[0] ? cmds.shift() : "main.js";
cmds.unshift(script);
cmds.unshift("--program");
cmds.unshift(arg0);
cmds.unshift("--cwd");
} else {
cmds.unshift(arg0);
cmds.unshift("--program");
}
cmds.unshift("spawn");
} }
Cmdline.orders[cmds[0]](cmds.slice(1)); Cmdline.orders[cmds[0]](cmds.slice(1));
} }
return cmd_args; return {
process: cmd_args,
encode: parse_args,
decode: unparse_args,
}

42
scripts/modules/debug.js Normal file
View File

@@ -0,0 +1,42 @@
var debug = this
debug.stack_depth[prosperon.DOC] = `Return the current stack depth.
:return: A number representing the stack depth.
`
debug.build_backtrace[prosperon.DOC] = `Build and return a backtrace of the current call stack.
:return: An object representing the call stack backtrace.
`
debug.closure_vars[prosperon.DOC] = `Return the closure variables for a given function.
:param fn: The function object to inspect.
:return: An object containing the closure variables.
`
debug.local_vars[prosperon.DOC] = `Return the local variables for a specific stack frame.
:param depth: The stack frame depth to inspect.
:return: An object containing the local variables at the specified depth.
`
debug.fn_info[prosperon.DOC] = `Return metadata about a given function.
:param fn: The function object to inspect.
:return: An object with metadata about the function.
`
debug.backtrace_fns[prosperon.DOC] = `Return an array of functions in the current backtrace.
:return: An array of function objects from the call stack.
`
debug.dump_obj[prosperon.DOC] = `Return a string representation of a given object.
:param obj: The object to dump.
:return: A string describing the object's contents.
`
return this

35
scripts/modules/device.js Normal file
View File

@@ -0,0 +1,35 @@
// helpful render devices. width and height in pixels; diagonal in inches.
return {
pc: { width: 1920, height: 1080 },
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
ds_top: { width: 400, height: 240, diagonal: 3.53 },
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
playdate: { width: 400, height: 240, diagonal: 2.7 },
switch: { width: 1280, height: 720, diagonal: 6.2 },
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
switch_oled: { width: 1280, height: 720, diagonal: 7 },
dsi: { width: 256, height: 192, diagonal: 3.268 },
ds: { width: 256, height: 192, diagonal: 3 },
dsixl: { width: 256, height: 192, diagonal: 4.2 },
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
gba: { width: 240, height: 160, diagonal: 2.9 },
gameboy: { width: 160, height: 144, diagonal: 2.48 },
gbc: { width: 160, height: 144, diagonal: 2.28 },
steamdeck: { width: 1280, height: 800, diagonal: 7 },
vita: { width: 960, height: 544, diagonal: 5 },
psp: { width: 480, height: 272, diagonal: 4.3 },
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
ps1: { width: 320, height: 240, diagonal: 5 },
ps2: { width: 640, height: 480 },
snes: { width: 256, height: 224 },
gamecube: { width: 640, height: 480 },
n64: { width: 320, height: 240 },
c64: { width: 320, height: 200 },
macintosh: { width: 512, height: 342 },
gamegear: { width: 160, height: 144, diagonal: 3.2 }
};

27
scripts/modules/dmon.js Normal file
View File

@@ -0,0 +1,27 @@
var dmon = this
dmon.watch[prosperon.DOC] = `Start watching the root directory, recursively.
This function begins monitoring the specified directory and its subdirectories recursively for events such as file creation, deletion, modification, or movement. Events are queued and can be retrieved by calling poll.
:return: None
:throws: An error if dmon is already watching.
`
dmon.unwatch[prosperon.DOC] = `Stop watching the currently monitored directory.
This function halts filesystem monitoring for the directory previously set by watch. It clears the watch state, allowing a new watch to be started.
:return: None
:throws: An error if no directory is currently being watched.
`
dmon.poll[prosperon.DOC] = `Retrieve and process queued filesystem events.
This function dequeues all pending filesystem events and invokes the provided callback for each one. The callback receives an event object with properties: 'action' (string: "create", "delete", "modify", or "move"), 'root' (string: watched directory), 'file' (string: affected file path), and 'old' (string: previous file path for move events, empty if not applicable).
:param callback: A function to call for each event, receiving an event object as its argument.
:return: None
`
return dmon

View File

@@ -3,122 +3,374 @@ var graphics = use('graphics')
var math = use('math') var math = use('math')
var util = use('util') var util = use('util')
var os = use('os') var os = use('os')
var geometry = use('geometry')
var draw = {} var draw = {}
draw[prosperon.DOC] = ` draw[prosperon.DOC] = `
A collection of 2D drawing functions that operate in screen space. Provides primitives A collection of 2D drawing functions that operate in screen space. Provides primitives
for lines, rectangles, text, sprite drawing, etc. for lines, rectangles, text, sprite drawing, etc. Immediate mode.
` `
var whiteimage = {} /*var whiteimage = {}
whiteimage.surface = graphics.make_surface([1,1]) whiteimage = graphics.make_surface([1,1])
whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1]) whiteimage.rect({x:0,y:0,width:1,height:1}, [1,1,1,1])
whiteimage.texture = prosperon.gpu.load_texture(whiteimage.surface) render.load_texture(whiteimage)
*/
draw.point = function (pos, size, color = Color.blue) { if (render.point)
render._main.point(pos, color) draw.point = function(pos,size,opt = {color:Color.white}, pipeline) {
} render.settings(opt)
render.pipeline(pipeline)
render.point([pos])
}
else
draw.point = function() { throw new Error('Backend cannot draw points.') }
draw.point[prosperon.DOC] = ` draw.point[prosperon.DOC] = `
:param pos: A 2D position ([x, y]) where the point should be drawn. :param pos: A 2D position ([x, y]) where the point should be drawn.
:param size: The size of the point (not currently affecting rendering). :param size: The size of the point.
:param color: The color of the point, defaults to Color.blue. :param color: The color of the point, defaults to white.
:return: None :return: None
` `
draw.line = function render_line(points, color = Color.white, thickness = 1, pipeline) { /* ------------------------------------------------------------------------
var mesh = graphics.make_line_prim(points, thickness, 0, 0, color) * helper is (dx,dy) inside the desired wedge?
render.queue({ * -------------------------------------------------------------------- */
type: 'geometry', function within_wedge(dx, dy, start, end, full_circle)
mesh, {
pipeline, if (full_circle) return true
first_index: 0,
num_indices: mesh.num_indices var ang = Math.atan2(dy, dx) // [-π,π]
}) if (ang < 0) ang += Math.PI * 2
var t = ang / (Math.PI * 2) // turn ∈ [0,1)
if (start <= end) return t >= start && t <= end
return t >= start || t <= end // wrap-around arc
} }
draw.line[prosperon.DOC] = `
:param points: An array of 2D positions representing the line vertices.
:param color: The color of the line, default Color.white.
:param thickness: The line thickness, default 1.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: None
`
draw.cross = function render_cross(pos, size, color = Color.red, thickness = 1, pipe) { /* ------------------------------------------------------------------------
* software ellipse outline / ring / fill via inner_radius
* -------------------------------------------------------------------- */
function software_ellipse(pos, radii, opt)
{
var rx = radii[0], ry = radii[1]
if (rx <= 0 || ry <= 0) return
var cx = pos[0], cy = pos[1]
var raw_start = opt.start
var raw_end = opt.end
var full_circle = Math.abs(raw_end - raw_start) >= 1 - 1e-9
var start = (raw_start % 1 + 1) % 1
var end = (raw_end % 1 + 1) % 1
var thickness = Math.max(1, opt.thickness|0)
/* inner ellipse radii (may be zero or negative → treat as no hole) */
var rx_i = rx - thickness,
ry_i = ry - thickness
var hole = (rx_i > 0 && ry_i > 0)
/* fast one-pixel outline ? --------------------------------------- */
if (!hole && thickness === 1) {
/* (same midpoint algorithm as before, filtered by wedge) --------*/
var rx_sq = rx * rx, ry_sq = ry * ry
var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1
var x = 0, y = ry, px = 0, py = two_rx_sq * y
var p = ry_sq - rx_sq * ry + 0.25 * rx_sq
function plot_pts(x, y) {
var pts = [
[cx + x, cy + y], [cx - x, cy + y],
[cx + x, cy - y], [cx - x, cy - y]
].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))
if (pts.length) render.point(pts)
}
while (px < py) {
plot_pts(x, y)
++x; px += two_ry_sq
if (p < 0) p += ry_sq + px
else { --y; py -= two_rx_sq; p += ry_sq + px - py }
}
p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq
while (y >= 0) {
plot_pts(x, y)
--y; py -= two_rx_sq
if (p > 0) p += rx_sq - py
else { ++x; px += two_ry_sq; p += rx_sq - py + px }
}
return
}
/* ------------------------------------------------------------------
* FILL or RING (thickness ≥ 2 OR hole == false -> full fill)
* ---------------------------------------------------------------- */
var strips = []
var rx_sq = rx * rx, ry_sq = ry * ry
var rx_i_sq = rx_i * rx_i, ry_i_sq = ry_i * ry_i
for (var dy = -ry; dy <= ry; ++dy) {
var yy = dy * dy
var x_out = Math.floor(rx * Math.sqrt(1 - yy / ry_sq))
var y_screen = cy + dy
/* optional inner span */
var x_in = hole ? Math.floor(rx_i * Math.sqrt(1 - yy / ry_i_sq)) : -1
var run_start = null
for (var dx = -x_out; dx <= x_out; ++dx) {
if (hole && Math.abs(dx) <= x_in) { run_start = null; continue }
if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue }
if (run_start === null) run_start = cx + dx
var last = (dx === x_out)
var next_in_ring =
!last &&
!(hole && Math.abs(dx+1) <= x_in) &&
within_wedge(dx+1, dy, start, end, full_circle)
if (last || !next_in_ring) {
strips.push({
x: run_start,
y: y_screen,
width: (cx + dx) - run_start + 1,
height: 1
})
run_start = null
}
}
}
if (strips.length) render.rects(strips)
}
var ellipse_def = {
color: Color.white,
start: 0,
end: 1,
mode: 'fill',
thickness: 1,
}
if (render.ellipse)
draw.ellipse = function(pos, radii, def, pipeline) {
}
else
draw.ellipse = function(pos, radii, def, pipeline) {
var opt = def ? {...ellipse_def, ...def} : ellipse_def
render.settings(opt)
render.pipeline(pipeline)
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
software_ellipse(pos, radii, opt)
}
var line_def = {
color: Color.white,
thickness: 1,
cap:"butt",
}
draw.line = function(points, def, pipeline)
{
var opt
if (def)
opt = {...line_def, ...def}
else
opt = line_def
render.settings(opt)
render.pipeline(pipeline)
render.line(points)
}
draw.cross = function render_cross(pos, size, def, pipe) {
var a = [pos.add([0, size]), pos.add([0, -size])] var a = [pos.add([0, size]), pos.add([0, -size])]
var b = [pos.add([size, 0]), pos.add([-size, 0])] var b = [pos.add([size, 0]), pos.add([-size, 0])]
draw.line(a, color, thickness) draw.line(a, def, pipe)
draw.line(b, color, thickness) draw.line(b, def,pipe)
} }
draw.cross[prosperon.DOC] = `
:param pos: The center of the cross as a 2D position ([x, y]).
:param size: Half the size of each cross arm.
:param color: The color of the cross, default Color.red.
:param thickness: The thickness of each line, default 1.
:param pipe: (Optional) A pipeline or rendering state object.
:return: None
`
draw.arrow = function render_arrow(start, end, color = Color.red, wingspan = 4, wingangle = 10, pipe) { draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, def, pipe) {
var dir = math.norm(end.sub(start)) var dir = math.norm(end.sub(start))
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end] var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end] var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
render.line([start, end], color) draw.line([start, end], def, pipe)
render.line(wing1, color) draw.line(wing1, def, pipe)
render.line(wing2, color) draw.line(wing2, def, pipe)
} }
draw.arrow[prosperon.DOC] = `
:param start: The start position of the arrow ([x, y]).
:param end: The end (tip) position of the arrow ([x, y]).
:param color: The color, default Color.red.
:param wingspan: The length of each arrowhead 'wing', default 4.
:param wingangle: Wing rotation in degrees, default 10.
:param pipe: (Optional) A pipeline or rendering state object.
:return: None
`
draw.rectangle = function render_rectangle(rect, color = Color.white, pipeline) { /* ------------------------------------------------------------------------
var T = os.make_transform() * helper plain rectangle outline of arbitrary thickness (radius=0)
T.rect(rect) * -------------------------------------------------------------------- */
render.queue({ function software_outline_rect(rect, thickness)
type: 'sprite', {
transform: T, if (thickness <= 0) {
color, render.rectangle(rect);
pipeline, return;
image: whiteimage }
})
/* stroke swallows the whole thing → fill instead */
if ((thickness << 1) >= rect.width ||
(thickness << 1) >= rect.height) {
render.rectangle(rect) // filled
return
}
const x0 = rect.x,
y0 = rect.y,
x1 = rect.x + rect.width,
y1 = rect.y + rect.height
render.rects([
{ x:x0, y:y0, width:rect.width, height:thickness }, // top
{ x:x0, y:y1-thickness, width:rect.width, height:thickness }, // bottom
{ x:x0, y:y0+thickness, width:thickness,
height:rect.height - (thickness<<1) }, // left
{ x:x1-thickness, y:y0+thickness, width:thickness,
height:rect.height - (thickness<<1) } // right
])
} }
draw.rectangle[prosperon.DOC] = `
:param rect: A rectangle object with {x, y, width, height}.
:param color: The fill color, default Color.white.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: None
`
var tile_def = {repeat_x:true, repeat_y:true} /* ------------------------------------------------------------------------
* ROUNDED rectangle outline (was software_round_rect)
* -------------------------------------------------------------------- */
function software_round_rect(rect, radius, thickness = 1)
{
if (thickness <= 0) {
software_fill_round_rect(rect, radius)
return
}
draw.tile = function(image, rect, color = Color.white, tile = tile_def, pipeline) { radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
if (!image) throw Error('Need an image to render.')
if (typeof image === "string") /* stroke covers whole rect → fall back to fill ------------------- */
image = graphics.texture(image) if ((thickness << 1) >= rect.width ||
var mesh = render._main.tile(image.texture, {x:0,y:0,width:image.texture.width,height:image.texture.height}, rect, tile) (thickness << 1) >= rect.height ||
render.queue({ thickness >= radius) {
type:'geometry', software_fill_round_rect(rect, radius)
mesh, return
image, }
pipeline,
first_index:0, const x0 = rect.x,
num_indices:mesh.num_indices y0 = rect.y,
}) x1 = rect.x + rect.width - 1, // inclusive
y1 = rect.y + rect.height - 1
const cx_l = x0 + radius, cx_r = x1 - radius
const cy_t = y0 + radius, cy_b = y1 - radius
const r_out = radius
const r_in = radius - thickness
/* straight bands (top/bottom/left/right) ------------------------- */
render.rects([
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1),
height:thickness }, // top
{ x:x0 + radius, y:y1 - thickness + 1,
width:rect.width - (radius << 1), height:thickness }, // bottom
{ x:x0, y:y0 + radius, width:thickness,
height:rect.height - (radius << 1) }, // left
{ x:x1 - thickness + 1, y:y0 + radius, width:thickness,
height:rect.height - (radius << 1) } // right
])
/* corner arcs ---------------------------------------------------- */
const strips = []
for (let dy = 0; dy < radius; ++dy) {
const dy_sq = dy * dy
const dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq))
const dx_in = (r_in > 0 && dy < r_in)
? Math.floor(Math.sqrt(r_in * r_in - dy_sq))
: -1 // no inner rim
const w = dx_out - dx_in // strip width
if (w <= 0) continue
/* top */
strips.push(
{ x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 }, // NW
{ x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 } // NE
)
/* bottom */
strips.push(
{ x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 }, // SW
{ x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 } // SE
)
}
render.rects(strips)
}
/* ------------------------------------------------------------------------
* filled rounded rect (unchanged)
* -------------------------------------------------------------------- */
function software_fill_round_rect(rect, radius)
{
radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
const x0 = rect.x,
y0 = rect.y,
x1 = rect.x + rect.width - 1,
y1 = rect.y + rect.height - 1
/* main column */
render.rects([
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1),
height:rect.height }
])
/* side columns */
render.rects([
{ x:x0, y:y0 + radius, width:radius,
height:rect.height - (radius << 1) },
{ x:x1 - radius + 1, y:y0 + radius, width:radius,
height:rect.height - (radius << 1) }
])
/* corner caps */
const cx_l = x0 + radius, cx_r = x1 - radius
const cy_t = y0 + radius, cy_b = y1 - radius
const caps = []
for (let dy = 0; dy < radius; ++dy) {
const dx = Math.floor(Math.sqrt(radius * radius - dy * dy))
const w = (dx << 1) + 1
caps.push(
{ x:cx_l - dx, y:cy_t - dy, width:w, height:1 },
{ x:cx_r - dx, y:cy_t - dy, width:w, height:1 },
{ x:cx_l - dx, y:cy_b + dy, width:w, height:1 },
{ x:cx_r - dx, y:cy_b + dy, width:w, height:1 }
)
}
render.rects(caps)
}
var rect_def = {
thickness:1,
color: Color.white,
radius: 0
}
draw.rectangle = function render_rectangle(rect, def, pipeline) {
var opt = def ? {...rect_def, ...def} : rect_def
render.settings(opt)
render.pipeline(pipeline)
var t = opt.thickness|0
if (t <= 0) {
if (opt.radius)
software_fill_round_rect(rect, opt.radius)
else
render.rectangle(rect)
return
}
if (opt.radius)
software_round_rect(rect, opt.radius, t)
else
software_outline_rect(rect,t)
} }
draw.tile[prosperon.DOC] = `
:param image: An image object or string path to a texture.
:param rect: A rectangle specifying draw location/size ({x, y, width, height}).
:param color: The color tint, default Color.white.
:param tile: A tiling definition ({repeat_x, repeat_y}), default tile_def.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: None
:raises Error: If no image is provided.
`
var slice9_info = { var slice9_info = {
tile_top:true, tile_top:true,
@@ -126,28 +378,21 @@ var slice9_info = {
tile_left:true, tile_left:true,
tile_right:true, tile_right:true,
tile_center_x:true, tile_center_x:true,
tile_center_right:true tile_center_right:true,
color: Color.white,
} }
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, color = Color.white, info = slice9_info, pipeline) { draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, pipeline) {
if (!image) throw Error('Need an image to render.') if (!image) throw Error('Need an image to render.')
if (typeof image === "string") if (typeof image === "string")
image = graphics.texture(image) image = graphics.texture(image)
var mesh = render._main.slice9(image.texture, rect, util.normalizeSpacing(slice), info)
render.queue({ render.slice9(image, rect, slice, slice9_info, pipeline);
type: 'geometry',
mesh,
image,
pipeline,
first_index:0,
num_indices:mesh.num_indices
})
} }
draw.slice9[prosperon.DOC] = ` draw.slice9[prosperon.DOC] = `
:param image: An image object or string path to a texture. :param image: An image object or string path to a texture.
:param rect: A rectangle specifying draw location/size, default [0, 0]. :param rect: A rectangle specifying draw location/size, default [0, 0].
:param slice: The pixel inset or spacing for the 9-slice (number or object). :param slice: The pixel inset or spacing for the 9-slice (number or object).
:param color: The color tint, default Color.white.
:param info: A slice9 info object controlling tiling of edges/corners. :param info: A slice9 info object controlling tiling of edges/corners.
:param pipeline: (Optional) A pipeline or rendering state object. :param pipeline: (Optional) A pipeline or rendering state object.
:return: None :return: None
@@ -155,99 +400,67 @@ draw.slice9[prosperon.DOC] = `
` `
var std_sprite_cmd = {type:'sprite', color:[1,1,1,1]} var std_sprite_cmd = {type:'sprite', color:[1,1,1,1]}
var image_info = {
tile_x: false,
tile_y: false,
flip_x: false,
flip_y: false,
color: Color.white,
mode: 'linear'
}
draw.image = function image(image, rect = [0,0], rotation = 0, color, pipeline) { draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], shear = [0,0], info = {}, pipeline) {
if (!image) throw Error('Need an image to render.') if (!image) throw Error('Need an image to render.')
if (typeof image === "string") if (typeof image === "string")
image = graphics.texture(image) image = graphics.texture(image)
rect.width ??= image.texture.width rect.width ??= image.texture.width
rect.height ??= image.texture.height rect.height ??= image.texture.height
var cmd = Object.create(std_sprite_cmd) info ??= image_info;
cmd.image = image render.settings(info)
cmd.rect = rect render.image(image, rect, rotation, anchor, shear, info)
if (pipeline) cmd.pipeline = pipeline
if (color) cmd.color = color
render.queue(cmd)
var sprite = graphics.make_sprite()
sprite.set_image(image)
sprite.set_rect(rect)
return sprite
} }
draw.image[prosperon.DOC] = `
:param image: An image object or string path to a texture.
:param rect: A rectangle specifying draw location/size, default [0,0]; width/height default to image size.
:param rotation: Rotation in degrees (not currently used).
:param color: The color tint, default none.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: A sprite object that was created for this draw call.
:raises Error: If no image is provided.
`
draw.images = function images(image, rects, config) { function software_circle(pos, radius)
if (!image) throw Error('Need an image to render.') {
if (typeof image === "string") image = graphics.texture(image) if (radius <= 0) return // nothing to draw
var bb = []
bb.width = image.texture.width var cx = pos[0], cy = pos[1]
bb.height = image.texture.height var x = 0, y = radius
var sprites = [] var d = 3 - (radius << 1) // decision parameter
for (var rect of rects) {
rect.__proto__ = bb while (x <= y) {
var sprite = graphics.make_sprite() draw.point([
sprite.set_rect(rect) [cx + x, cy + y], [cx - x, cy + y],
sprite.set_image(image) [cx + x, cy - y], [cx - x, cy - y],
sprites.push(sprite) [cx + y, cy + x], [cx - y, cy + x],
[cx + y, cy - x], [cx - y, cy - x]
])
if (d < 0) d += (x << 2) + 6
else {
d += ((x - y) << 2) + 10
y--
}
x++
} }
var cmds = graphics.make_sprite_queue(sprites, prosperon.camera, undefined)
for (var i = 0; i < cmds.length; i++)
render.queue(cmds[i])
return sprites
} }
draw.images[prosperon.DOC] = `
:param image: An image object or string path to a texture.
:param rects: An array of rectangle objects ({x, y, width, height}) to draw.
:param config: (Unused) Additional config data if needed.
:return: An array of sprite objects created and queued for rendering.
:raises Error: If no image is provided.
`
draw.sprites = function(sprites, sort = 0, pipeline) { var circle_def = {
var cmds = graphics.make_sprite_queue(sprites, prosperon.camera, pipeline, sort) inner_radius:1, // percentage: 1 means filled circle
for (var i = 0; i < cmds.length; i++) color: Color.white,
render.queue(cmds[i]) start:0,
end: 1,
} }
draw.sprites[prosperon.DOC] = ` draw.circle = function render_circle(pos, radius, def, pipeline) {
:param sprites: An array of sprite objects to draw. draw.ellipse(pos, [radius,radius], def, pipeline)
:param sort: Sorting mode or order, default 0.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: None
`
draw.circle = function render_circle(pos, radius, color, inner_radius = 1, pipeline) {
render.rectangle({x:pos.x, y:pos.y, width:radius*2,height:radius*2}, color, circle_pipeline)
} }
draw.circle[prosperon.DOC] = `
:param pos: Center of the circle ([x, y]).
:param radius: The circle radius.
:param color: The fill color of the circle, default none.
:param inner_radius: (Unused) Possibly ring thickness, default 1.
:param pipeline: (Optional) A pipeline or rendering state object.
:return: None
`
var sysfont = graphics.get_font('fonts/c64.ttf', 8) var sysfont = graphics.get_font('fonts/c64.ttf', 8)
draw.text = function text(text, rect, font = sysfont, size = 0, color = Color.white, wrap = 0, pipeline) { draw.text = function text(text, rect, font = sysfont, size = 0, color = Color.white, wrap = 0, pipeline) {
if (typeof font === 'string') font = graphics.get_font(font) if (typeof font === 'string') font = graphics.get_font(font)
var mesh = graphics.make_text_buffer(text, rect, 0, color, wrap, font) var mesh = graphics.make_text_buffer(text, rect, 0, color, wrap, font)
render.queue({ render.geometry(font, mesh)
type: 'geometry',
mesh,
image: font,
texture: font.texture,
pipeline,
first_index:0,
num_indices:mesh.num_indices
})
} }
draw.text[prosperon.DOC] = ` draw.text[prosperon.DOC] = `
:param text: The string to draw. :param text: The string to draw.

View File

@@ -1,36 +1,33 @@
// loop.js /*
var render = use('render') // The refactored file above the "dull" game engine
This sets up a lot of different modules to be used altogether
*/
var render = use('render')
var layout = use('clay') var layout = use('clay')
var input = use('input') var input = use('input')
var emitter = use('emitter') var emitter = use('emitter')
var os = use('os') var os = use('os')
var event = use('event')
var imgui = use('imgui') var imgui = use('imgui')
var tracy = use('tracy') var tracy = use('tracy')
var waittime = 1/240
var last_frame_time = 0 var last_frame_time = 0
var frame_t = 0
var fpses = []
var timescale = 1 var timescale = 1
last_frame_time = os.now()
function step() { function step() {
var now = os.now() var now = os.now()
var dt = now - last_frame_time var dt = now - last_frame_time
if (dt < waittime) os.sleep(waittime - dt) last_frame_time = now
last_frame_time = os.now()
dt = last_frame_time - frame_t // event.engine_input(e => prosperon.dispatch(e.type, e))
frame_t = last_frame_time
event.engine_input(e => prosperon.dispatch(e.type, e))
layout.newframe() layout.newframe()
prosperon.appupdate(dt) prosperon.appupdate(dt)
input.procdown() input.procdown()
emitter.update(dt * timescale) emitter.update(dt * timescale)
os.update_timers(dt * timescale)
prosperon.update(dt * timescale) prosperon.update(dt * timescale)
render.setup_draw() render.setup_draw()
@@ -39,12 +36,18 @@ function step() {
if (imgui) imgui.prosperon_menu(); if (imgui) imgui.prosperon_menu();
// Now do the GPU present (calls gpupresent in render.js) // Now do the GPU present (calls gpupresent in render.js)
render._main.present() render.present()
tracy.end_frame() tracy.end_frame()
} }
function start()
{
}
// Return or export them so you can call from a main script // Return or export them so you can call from a main script
return { return {
start,
step step
} }

151
scripts/modules/enet.js Normal file
View File

@@ -0,0 +1,151 @@
// enet.js doc (updated)
var enet = this;
//------------------------------------------------
// Top-level ENet functions
//------------------------------------------------
enet.initialize[prosperon.DOC] = `
Initialize the ENet library. Must be called before using any ENet functionality.
Throws an error if initialization fails.
:return: None
`;
enet.deinitialize[prosperon.DOC] = `
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
need any ENet functionality.
:return: None
`;
enet.create_host[prosperon.DOC] = `
Create an ENet host for either a client-like unbound host or a server bound to a specific
address and port:
- If no argument is provided, creates an unbound "client-like" host with default settings
(maximum 32 peers, 2 channels, unlimited bandwidth).
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
Throws an error if host creation fails for any reason.
:param address: (optional) A string in 'ip:port' format to bind the host (server), or
omit to create an unbound client-like host.
:return: An ENetHost object.
`;
//------------------------------------------------
// ENetHost methods
//------------------------------------------------
var enet_host = prosperon.c_types.enet_host;
enet_host.service[prosperon.DOC] = `
Poll for and process any available network events (connect, receive, disconnect, or none)
from this host, calling the provided callback for each event. This function loops until
no more events are available in the current timeframe.
Event object properties:
- type: String, one of "connect", "receive", "disconnect", or "none".
- peer: (present if type = "connect") The ENetPeer object for the new connection.
- channelID: (present if type = "receive") The channel on which the data was received.
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
:param callback: A function called once for each available event, receiving an event
object as its single argument.
:param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
:return: None
`;
enet_host.connect[prosperon.DOC] = `
Initiate a connection from this host to a remote server. Throws an error if the
connection cannot be started.
:param host: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
:param port: The port number to connect to.
:return: An ENetPeer object representing the connection.
`;
enet_host.flush[prosperon.DOC] = `
Flush all pending outgoing packets for this host immediately.
:return: None
`;
enet_host.broadcast[prosperon.DOC] = `
Broadcast a JavaScript object to all connected peers on channel 0. The object is
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
:param data: A JavaScript object to broadcast to all peers.
:return: None
`;
//------------------------------------------------
// ENetPeer methods
//------------------------------------------------
var enet_peer = prosperon.c_types.enet_peer;
enet_peer.send[prosperon.DOC] = `
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
sent reliably. Throws an error if serialization fails.
:param data: A JavaScript object to send.
:return: None
`;
enet_peer.disconnect[prosperon.DOC] = `
Request a graceful disconnection from this peer. The connection will close after
pending data is sent.
:return: None
`;
enet_peer.disconnect_now[prosperon.DOC] = `
Immediately terminate the connection to this peer, discarding any pending data.
:return: None
`;
enet_peer.disconnect_later[prosperon.DOC] = `
Request a disconnection from this peer after all queued packets are sent.
:return: None
`;
enet_peer.reset[prosperon.DOC] = `
Reset this peer's connection, immediately dropping it and clearing its internal state.
:return: None
`;
enet_peer.ping[prosperon.DOC] = `
Send a ping request to this peer to measure latency.
:return: None
`;
enet_peer.throttle_configure[prosperon.DOC] = `
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
rate based on packet loss or congestion.
:param interval: The interval (ms) between throttle adjustments.
:param acceleration: The factor to increase sending speed when conditions improve.
:param deceleration: The factor to decrease sending speed when conditions worsen.
:return: None
`;
enet_peer.timeout[prosperon.DOC] = `
Set timeout parameters for this peer, determining how long ENet waits before considering
the connection lost.
:param timeout_limit: The total time (ms) before the peer is disconnected.
:param timeout_min: The minimum timeout (ms) used for each timeout attempt.
:param timeout_max: The maximum timeout (ms) used for each timeout attempt.
:return: None
`;
// Return the enet object.
return enet;

View File

@@ -1,6 +1,7 @@
var cam = {} var cam = {}
var os = use('os') var os = use('os')
var transform = use('transform')
var basecam = {} var basecam = {}
basecam.draw_rect = function(size) basecam.draw_rect = function(size)
@@ -88,7 +89,7 @@ function mode_rect(src,dst,mode = "stretch")
cam.make = function() cam.make = function()
{ {
var c = Object.create(basecam) var c = Object.create(basecam)
c.transform = os.make_transform() c.transform = new transform
c.transform.unit() c.transform.unit()
c.zoom = 1 c.zoom = 1
c.size = [640,360] c.size = [640,360]

View File

@@ -1,7 +1,7 @@
var Color = use('color') var Color = use('color')
var os = use('os') var os = use('os')
var graphics = use('graphics') var graphics = use('graphics')
//var render = use('render') var transform = use('transform')
var ex = {} var ex = {}
@@ -77,7 +77,7 @@ ex.spawn = function(t)
} }
par = { par = {
transform: os.make_transform(), transform: new transform,
life: this.life, life: this.life,
time: 0, time: 0,
color: this.color, color: this.color,
@@ -111,7 +111,7 @@ ex.draw = function()
/* var diff = graphics.texture(this.diffuse) /* var diff = graphics.texture(this.diffuse)
if (!diff) throw new Error("emitter does not have a proper diffuse texture") if (!diff) throw new Error("emitter does not have a proper diffuse texture")
var mesh = render._main.make_sprite_mesh(this.particles) var mesh = graphics.make_sprite_mesh(this.particles)
if (mesh.num_indices === 0) return if (mesh.num_indices === 0) return
render.queue({ render.queue({
type:'geometry', type:'geometry',

View File

@@ -184,7 +184,9 @@ sprite.inputs.kp1 = function () {
this.setanchor("ul"); this.setanchor("ul");
}; };
var tree = graphics.make_rtree() var rtree = use('rtree')
var tree = new rtree
sprite.tree = tree; sprite.tree = tree;
var IN = Symbol() var IN = Symbol()
@@ -222,15 +224,15 @@ return sprite;
--- ---
var Color = use('color') var Color = use('color')
var os = use('os') var transform = use('transform')
var graphics = use('graphics') var sprite = use('sprite')
this.transform = os.make_transform(); this.transform = new transform;
if (this.overling.transform) if (this.overling.transform)
this.transform.parent = this.overling.transform; this.transform.parent = this.overling.transform;
this.transform.change_hook = $.t_hook; this.transform.change_hook = $.t_hook;
var msp = graphics.make_sprite(); var msp = new sprite
this._sprite = msp; this._sprite = msp;
msp.color = Color.white; msp.color = Color.white;
this.transform.sprite = this this.transform.sprite = this

View File

@@ -1,33 +0,0 @@
var ret = {
get pos() {
if (!this.transform) return
return this.transform.pos;
},
set pos(x) {
this.transform.pos = x;
},
get angle() {
return this.transform.angle;
},
set angle(x) {
this.transform.angle = x;
},
get scale() {
return this.transform.scale;
},
set scale(x) {
this.transform.scale = x;
},
move(vec) {
this.pos = this.pos.add(vec);
},
rotate(x) {
this.transform.rotate([0, 0, -1],x);
},
grow(vec) {
if (typeof vec === "number") vec = [vec, vec];
this.scale = this.scale.map((x, i) => x * vec[i]);
},
}
return ret

View File

@@ -1,4 +1,5 @@
var graphics = this var graphics = this
graphics[prosperon.DOC] = ` graphics[prosperon.DOC] = `
Provides functionality for loading and managing images, fonts, textures, and sprite meshes. Provides functionality for loading and managing images, fonts, textures, and sprite meshes.
Includes both JavaScript and C-implemented routines for creating geometry buffers, performing Includes both JavaScript and C-implemented routines for creating geometry buffers, performing
@@ -6,50 +7,151 @@ rectangle packing, etc.
` `
var io = use('io') var io = use('io')
var os = use('os')
var res = use('resources') var res = use('resources')
var render = use('render')
var GPU = Symbol()
var CPU = Symbol()
var LASTUSE = Symbol()
var cache = new Map()
// When creating an image, do an Object.create(graphics.Image)
graphics.Image = {
get gpu() {
this[LASTUSE] = os.now();
if (!this[GPU]) {
this[GPU] = render.load_texture(this[CPU]);
decorate_rect_px(this);
}
return this[GPU]
},
get texture() { return this.gpu },
get cpu() {
this[LASTUSE] = os.now();
if (!this[CPU]) this[CPU] = render.read_texture(this[GPU])
return this[CPU]
},
get surface() { return this.cpu },
get width() {
return this[GPU].width
},
get height() {
return this[GPU].height
},
unload_gpu() {
this[GPU] = undefined
},
unload_cpu() {
this[CPU] = undefined
},
}
function calc_image_size(img) { function calc_image_size(img) {
if (!img.texture || !img.rect) return if (!img.texture || !img.rect) return
return [img.texture.width * img.rect.width, img.texture.height * img.rect.height] return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
} }
/** function decorate_rect_px(img) {
Internally loads image data from disk and prepares a GPU texture. Used by graphics.texture(). // needs a GPU texture to measure
Not intended for direct user calls. if (!img || !img.texture) return
*/
function create_image(path) { // default UV rect is the whole image if none supplied
var data = io.slurpbytes(path) img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1
var newimg
switch (path.ext()) { // store pixel-space version: [x, y, w, h] in texels
case 'gif': img.rect_px = {
newimg = graphics.make_gif(data) x:Math.round(img.rect.x * img.texture.width),
if (newimg.surface) y:Math.round(img.rect.y * img.texture.height),
newimg.texture = prosperon.gpu.load_texture(newimg.surface) width:Math.round(img.rect.width * img.texture.width),
else height:Math.round(img.rect.height * img.texture.height)
for (var frame of newimg.frames) }
frame.texture = prosperon.gpu.load_texture(frame.surface) }
break
case 'ase': function make_handle(obj)
case 'aseprite': {
newimg = graphics.make_aseprite(data) var image = Object.create(graphics.Image);
if (newimg.surface)
newimg.texture = prosperon.gpu.load_texture(newimg.surface) if (obj.surface) {
else { im
for (var anim in newimg) { }
var a = newimg[anim] return Object.assign(Object.create(graphics.Image), {
for (var frame of a.frames) rect:{x:0,y:0,width:1,height:1},
frame.texture = prosperon.gpu.load_texture(frame.surface) [CPU]:obj,
} [GPU]:undefined,
} [LASTUSE]:os.now()
break })
default: }
newimg = {
surface: graphics.make_texture(data) function wrapSurface(surf, maybeRect){
} const h = make_handle(surf);
newimg.texture = prosperon.gpu.load_texture(newimg.surface) if(maybeRect) h.rect = maybeRect; /* honour frame sub-rect */
break return h;
}
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
return arr.map(f => ({
image : wrapSurface(f.surface || f), /* accept bare surface too */
time: f.time,
rect: f.rect /* keep for reference */
}));
}
function makeAnim(frames, loop=true){
return { frames, loop }
}
function decode_image(bytes, ext)
{
switch(ext) {
case 'gif': return graphics.make_gif(bytes)
case 'ase':
case 'aseprite': return graphics.make_aseprite(bytes)
default: return {surface:graphics.make_texture(bytes)}
}
}
function create_image(path){
try{
const bytes = io.slurpbytes(path);
let raw = decode_image(bytes, path.ext());
/* ── Case A: static image ─────────────────────────────────── */
if(raw.surface)
return make_handle(raw.surface);
/* ── Case B: GIF helpers returned array [surf, …] ─────────── */
if(Array.isArray(raw))
return makeAnim( wrapFrames(raw), true );
/* ── Case C: GIF helpers returned {frames,loop} ───────────── */
if(raw.frames && Array.isArray(raw.frames))
return makeAnim( wrapFrames(raw.frames), !!raw.loop );
/* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */
const anims = {};
for(const [name, anim] of Object.entries(raw)){
if(anim && Array.isArray(anim.frames))
anims[name] = makeAnim( wrapFrames(anim.frames), !!anim.loop );
else if(anim && anim.surface) /* ase with flat surface */
anims[name] = makeAnim(
[{image:make_handle(anim.surface),time:0}], true );
}
if(Object.keys(anims).length) return anims;
throw new Error('Unsupported image structure from decoder');
}catch(e){
console.error(`Error loading image ${path}: ${e.message}`);
throw e;
} }
return newimg
} }
var image = {} var image = {}
@@ -82,15 +184,49 @@ graphics.is_image[prosperon.DOC] = `
:return: True if 'obj' has a .texture and a .rect property, indicating it's an image object. :return: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
` `
graphics.texture = function texture(path) { graphics.texture_from_data = function(data)
if (typeof path !== 'string') { {
return path // fallback if already an image object if (!(data instanceof ArrayBuffer)) return undefined
throw new Error('need a string for graphics.texture')
var img = {
surface: graphics.make_texture(data)
} }
var parts = path.split(':') render.load_texture(img)
var ipath = res.find_image(parts[0]) decorate_rect_px(img)
graphics.texture.cache[ipath] ??= create_image(ipath)
return graphics.texture.cache[ipath] return img
}
graphics.from_surface = function(id, surf)
{
return make_handle(surf)
var img = { surface: surf }
}
graphics.from = function(id, data)
{
if (typeof id !== 'string')
throw new Error('Expected a string ID')
if (data instanceof ArrayBuffer)
return graphics.texture_from_data(data)
}
graphics.texture = function texture(path) {
if (path.__proto__ === graphics.Image) return path
if (typeof path !== 'string')
throw new Error('need a string for graphics.texture')
var id = path.split(':')[0]
if (cache.has(id)) return cache.get(id)
var ipath = res.find_image(id)
if (!ipath) throw new Error(`unknown image ${id}`)
var image = create_image(ipath)
cache.set(id, image)
return image
} }
graphics.texture[prosperon.DOC] = ` graphics.texture[prosperon.DOC] = `
:param path: A string path to an image file or an already-loaded image object. :param path: A string path to an image file or an already-loaded image object.
@@ -170,9 +306,15 @@ graphics.get_font = function get_font(path, size) {
if (fontcache[fontstr]) return fontcache[fontstr] if (fontcache[fontstr]) return fontcache[fontstr]
var data = io.slurpbytes(fullpath) var data = io.slurpbytes(fullpath)
fontcache[fontstr] = graphics.make_font(data, size) var font = graphics.make_font(data,size)
fontcache[fontstr].texture = prosperon.gpu.load_texture(fontcache[fontstr].surface) font.texture = render.load_texture(font.surface)
return fontcache[fontstr]
console.log('loaded font texture')
console.log(json.encode(font.texture))
fontcache[fontstr] = font
return font
} }
graphics.get_font[prosperon.DOC] = ` graphics.get_font[prosperon.DOC] = `
:param path: A string path to a font file, optionally with ".size" appended. :param path: A string path to a font file, optionally with ".size" appended.
@@ -235,11 +377,6 @@ graphics.rectpack[prosperon.DOC] = `
Perform a rectangle packing using the stbrp library. Return positions for each rect. Perform a rectangle packing using the stbrp library. Return positions for each rect.
` `
graphics.make_rtree[prosperon.DOC] = `
:return: An R-Tree object for quickly querying many rectangles or sprite bounds.
Create a new R-Tree for geometry queries.
`
graphics.make_texture[prosperon.DOC] = ` graphics.make_texture[prosperon.DOC] = `
:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer. :param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering. :return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
@@ -265,13 +402,6 @@ graphics.cull_sprites[prosperon.DOC] = `
Filter an array of sprites to only those visible in the provided cameras view. Filter an array of sprites to only those visible in the provided cameras view.
` `
graphics.rects_to_sprites[prosperon.DOC] = `
:param rects: An array of rect coords or objects.
:param image: An image object (with .texture).
:return: An array of sprite objects referencing the 'image' and each rect for UV or position.
Convert an array of rect coords into sprite objects referencing a single image.
`
graphics.make_surface[prosperon.DOC] = ` graphics.make_surface[prosperon.DOC] = `
:param dimensions: The size object {width, height}, or an array [w,h]. :param dimensions: The size object {width, height}, or an array [w,h].
:return: A blank RGBA surface with the given dimensions, typically for software rendering or icons. :return: A blank RGBA surface with the given dimensions, typically for software rendering or icons.
@@ -290,11 +420,6 @@ graphics.make_font[prosperon.DOC] = `
Load a font from TTF/OTF data at the given size. Load a font from TTF/OTF data at the given size.
` `
graphics.make_sprite[prosperon.DOC] = `
:return: A new sprite object, which typically has .rect, .color, .layer, .image, etc.
Create a new sprite object, storing default properties.
`
graphics.make_line_prim[prosperon.DOC] = ` graphics.make_line_prim[prosperon.DOC] = `
:param points: An array of [x,y] points forming the line. :param points: An array of [x,y] points forming the line.
:param thickness: The thickness (width) of the polyline. :param thickness: The thickness (width) of the polyline.

18
scripts/modules/http.js Normal file
View File

@@ -0,0 +1,18 @@
var http = this
http.fetch[prosperon.DOC] = `Initiate an asynchronous HTTP GET request.
This function enqueues an HTTP GET request for the specified URL. It supports both buffered responses (full response collected in memory) and streaming data (processed as it arrives). The request is executed asynchronously, and its completion or streaming data is handled via callbacks provided in the options. Use 'poll' to process the results.
:param url: A string representing the URL to fetch.
:param options: Either a callback function or an object with optional properties:
- 'callback': A function invoked upon request completion, receiving an object with 'data' (string or null) and 'error' (string or null) properties.
- 'on_data': A function invoked for each chunk of streaming data, receiving a string chunk as its argument. If supplied, 'callback.data' will be null.
:return: undefined
:throws:
- An error if the URL is not a string or is invalid.
- An error if the options argument is neither a function nor an object.
- An error if memory allocation or CURL initialization fails.
`
return http

View File

@@ -2,6 +2,15 @@ var imgui = this;
var debug = {} var debug = {}
var imdebug = function imdebug() {
imtoggle("Physics", debug, "draw_phys");
imtoggle("Bouning boxes", debug, "draw_bb");
imtoggle("Names", debug, "draw_names");
imtoggle("Sprite nums", debug, "sprite_nums");
imtoggle("Debug overlay", debug, "show");
imtoggle("Show ur names", debug, "urnames");
};
function imtoggle(name, obj, field) { function imtoggle(name, obj, field) {
var changed = false; var changed = false;
var old = obj[field]; var old = obj[field];
@@ -10,9 +19,14 @@ function imtoggle(name, obj, field) {
return false; return false;
}; };
var render_menu = true;
imgui.render_menu = function(render) { render_menu = render; }
imgui.prosperon_menu = function prosperon_menu() { imgui.prosperon_menu = function prosperon_menu() {
imgui.newframe(); imgui.newframe();
if (render_menu) {
if (debug.console) if (debug.console)
debug.console = imgui.window("console", _ => { debug.console = imgui.window("console", _ => {
imgui.text(console.transcript); imgui.text(console.transcript);
@@ -42,13 +56,13 @@ imgui.prosperon_menu = function prosperon_menu() {
imtoggle("Console [f9]", debug, "console"); imtoggle("Console [f9]", debug, "console");
}); });
imgui.sokol_gfx(); // imgui.sokol_gfx();
imgui.menu("Graphics", _ => { imgui.menu("Graphics", _ => {
imtoggle("Draw sprites", render, "draw_sprites"); // imtoggle("Draw sprites", render, "draw_sprites");
imtoggle("Draw particles", render, "draw_particles"); // imtoggle("Draw particles", render, "draw_particles");
imtoggle("Draw HUD", render, "draw_hud"); // imtoggle("Draw HUD", render, "draw_hud");
imtoggle("Draw GUI", render, "draw_gui"); // imtoggle("Draw GUI", render, "draw_gui");
imgui.menu("Window", _ => { imgui.menu("Window", _ => {
prosperon.fullscreen = imgui.checkbox("fullscreen", prosperon.fullscreen); prosperon.fullscreen = imgui.checkbox("fullscreen", prosperon.fullscreen);
@@ -81,9 +95,8 @@ imgui.prosperon_menu = function prosperon_menu() {
} }
}); });
*/ */
// prosperon.imgui(); }
prosperon.imgui();
imgui.endframe(render._main);
}; };
imgui.windowpos[prosperon.DOC] = `Return the position of the current ImGui window as an ImVec2. imgui.windowpos[prosperon.DOC] = `Return the position of the current ImGui window as an ImVec2.
@@ -309,16 +322,6 @@ imgui.barplot[prosperon.DOC] = `Plot a bar chart in the current ImPlot.
:return: None :return: None
`; `;
imgui.pieplot[prosperon.DOC] = `Plot a pie chart in the current ImPlot.
:param labels: An array of label strings for each slice.
:param values: An array of numeric values corresponding to each slice.
:param x: The x position of the pies center.
:param y: The y position of the pies center.
:param radius: The radius of the pie chart.
:return: None
`;
imgui.textplot[prosperon.DOC] = `Render text at the specified coordinates in plot space. imgui.textplot[prosperon.DOC] = `Render text at the specified coordinates in plot space.
:param text: The string to render. :param text: The string to render.
@@ -725,4 +728,4 @@ imgui.init[prosperon.DOC] = `Initialize ImGui with SDL and SDL_gpu, creating the
:return: None :return: None
`; `;
return false return this

View File

@@ -103,8 +103,10 @@ io.basedir[prosperon.DOC] = `Return the application's base directory (where the
:return: A string with the base directory path. :return: A string with the base directory path.
` `
io.userdir[prosperon.DOC] = `Return the user's directory, often used for saving data. io.prefdir[prosperon.DOC] = `Get the user-and-app-specific path where files can be written.
:param org: The name of your organization.
:param app: The name of your application.
:return: A string with the user's directory path. :return: A string with the user's directory path.
` `

View File

@@ -4,6 +4,8 @@ Provides functions for introspecting and configuring the QuickJS runtime engine.
Includes debug info, memory usage, GC controls, code evaluation, etc. Includes debug info, memory usage, GC controls, code evaluation, etc.
` `
js.rt_info[prosperon.DOC] = "Return internal QuickJS runtime info, such as object counts."
js.dump_shapes[prosperon.DOC] = ` js.dump_shapes[prosperon.DOC] = `
:return: A debug string describing the internal shape hierarchy used by QuickJS. :return: A debug string describing the internal shape hierarchy used by QuickJS.
Use this for internal debugging of object shapes. Use this for internal debugging of object shapes.

View File

@@ -1,8 +1,8 @@
var json = {} var json = {}
json.encode = function(val,space,replacer,whitelist) json.encode = function encode(val,replacer,space = 1,whitelist)
{ {
return JSON.stringify(val, replacer, space ? 1 : 0) return JSON.stringify(val, replacer, space)
} }
json.encode[prosperon.DOC] = `Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified. json.encode[prosperon.DOC] = `Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified.
@@ -11,7 +11,7 @@ If the record does not have a json() method, and if whitelist is a record, then
If the space input is true, then line breaks and extra whitespace will be included in the text.` If the space input is true, then line breaks and extra whitespace will be included in the text.`
json.decode = function(text,reviver) json.decode = function decode(text,reviver)
{ {
return JSON.parse(text,reviver) return JSON.parse(text,reviver)
} }

View File

@@ -0,0 +1,107 @@
var sprite = {}
var graphics = use('graphics')
var render = use('render')
var draw2d = use('draw2d')
var sprite = use('sprite')
var SPRITE = Symbol() /* raw C sprite */
var POS = Symbol() /* cached JS copies of simple data */
var ROT = Symbol()
var SCALE = Symbol()
var SKEW = Symbol()
var CENTER = Symbol()
var COLOR = Symbol()
var IMAGE = Symbol()
var ursprite = {
get pos() { return this[POS] },
set pos(v) {
this[POS] = v
this[SPRITE].pos = v
},
get center() { return this[CENTER] },
set center(v){
this[CENTER] = v
this[SPRITE].center = v
},
get rotation() { return this[ROT] },
set rotation(t){
this[ROT] = t
this[SPRITE].rotation = t * 2*Math.PI /* C expects radians */
this[SPRITE].set_affine()
},
get scale() { return this[SCALE] },
set scale(v){
this[SCALE] = v
this[SPRITE].scale = v
this[SPRITE].set_affine()
},
get skew() { return this[SKEW] },
set skew(v){
this[SKEW] = v
this[SPRITE].skew = v
this[SPRITE].set_affine()
},
get layer() { return this[SPRITE].layer },
set layer(n){ this[SPRITE].layer = n },
get color() { return this[COLOR] },
set color(v){
this[COLOR] = v
this[SPRITE].color = v
},
move(mv) { this[SPRITE].move(mv) },
moveto(p){
this.pos = p
},
get image() { return this[IMAGE] },
set image(img) {
this[IMAGE] = img
this[SPRITE].set_image(img)
}
}
var _sprites = []
var def_sprite = Object.freeze({
pos: [0,0],
rotation: 0,
scale: [1,1],
center: [0,0],
color: [1,1,1,1],
skew: [0,0],
layer: 0,
})
sprite.create = function(image, info) {
info.__proto__ = def_sprite
var sp = Object.create(ursprite)
sp[SPRITE] = new sprite(info)
sp.image = graphics.texture(image)
_sprites.push(sp)
return sp
}
sprite.forEach = fn => { for (let s of _sprites) fn(s) }
sprite.values = () => _sprites.slice()
sprite.geometry= () => graphics.make_sprite_mesh(_sprites)
var raws = []
sprite.queue = function() {
if (raws.length != _sprites.length)
raws.length = _sprites.length
for (var i = 0; i < _sprites.length; i++) raws[i] = _sprites[i]
return graphics.make_sprite_queue(_sprites.map(x => x[SPRITE]))
}
return sprite

114
scripts/modules/moth.js Normal file
View File

@@ -0,0 +1,114 @@
/**
* Moth Game Framework
* Higher-level game development framework built on top of Prosperon
*/
// Check if we received the required delay function
if (!arg || arg.length === 0 || typeof arg[0] !== 'function') {
throw new Error("moth module requires $_.delay function as first argument. Usage: use('moth', $_.delay)");
}
// Verify it's actually a delay function by checking if it has the expected behavior
var delay = arg[0];
if (!delay.name || (delay.name !== 'delay' && !delay.name.includes('delay'))) {
console.warn("Warning: The function passed to moth module may not be $_.delay");
}
var os = use('os');
var io = use('io');
var render = use('render');
var actor = use('actor');
var transform = use('transform');
var gameConfig = {};
var gameDir = "";
// Framework initialization
function initialize() {
var configPath = `config.js`;
if (io.exists(configPath))
gameConfig = use(configPath);
// Set up default config values
gameConfig.resolution = gameConfig.resolution || { width: 640, height: 480 };
gameConfig.internal_resolution = gameConfig.internal_resolution || gameConfig.resolution;
gameConfig.title = gameConfig.title || "Moth Game";
gameConfig.fps = gameConfig.fps || 60;
gameConfig.clearColor = gameConfig.clearColor || [0, 0, 0, 1];
// Initialize render system
render.initialize({
width: gameConfig.resolution.width,
height: gameConfig.resolution.height,
resolution_x: gameConfig.internal_resolution.width,
resolution_y: gameConfig.internal_resolution.height,
mode: gameConfig.mode || 'letterbox'
});
// Set up default camera
gameConfig.camera = gameConfig.camera || {
size: [gameConfig.internal_resolution.width, gameConfig.internal_resolution.height],
transform: new transform,
fov: 50,
near_z: 0,
far_z: 1000,
surface: undefined,
viewport: {x: 0, y: 0, width: 1, height: 1},
ortho: true,
anchor: [0, 0]
};
// Set window title
prosperon.window.title = gameConfig.title;
startGameLoop();
}
// Main game loop
function startGameLoop() {
var last = os.now();
var fpsTimer = 0;
var fpsCount = 0;
var targetFrameTime = 1 / gameConfig.fps;
function loop() {
var now = os.now();
var dt = now - last;
last = now;
// Dispatch update event
prosperon.dispatch('update', dt);
// Clear and set up rendering
render.clear(gameConfig.clearColor);
render.camera(gameConfig.camera);
// Dispatch draw event
prosperon.dispatch('draw');
// Present the frame
render.present();
// FPS counter
fpsTimer += dt;
fpsCount++;
if (fpsTimer >= 0.5) {
// Only update if the title wasn't changed by the game
if (prosperon.window.title === gameConfig.title) {
prosperon.window.title = `${gameConfig.title} - FPS ${(fpsCount / fpsTimer).toFixed(1)}`;
}
fpsTimer = fpsCount = 0;
}
// Schedule next frame
delay(loop, Math.max(0, targetFrameTime - (os.now() - now)));
}
loop();
}
// Public API
return {
initialize: initialize,
config: gameConfig
};

40
scripts/modules/nota.js Normal file
View File

@@ -0,0 +1,40 @@
var nota = this
var json = use('json')
var encode = nota.encode
function nota_tostring()
{
return json.encode(nota.decode(this))
}
var nota_obj = {
toString: nota_tostring
}
nota.encode = function(obj, replacer)
{
var result = encode(obj,replacer)
result.toString = nota_tostring
return result
}
nota.encode[prosperon.DOC] = `Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
:param value: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
:return: An ArrayBuffer containing the NOTA-encoded data.
:throws: An error if no argument is provided.
`
nota.decode[prosperon.DOC] = `Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
:param buffer: An ArrayBuffer containing NOTA-encoded data to decode.
:return: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
`
return nota

View File

@@ -1,8 +1,5 @@
var os = this var os = this
os.make_transform[prosperon.DOC] = "Create a new transform object that can be used for 2D/3D positioning, scaling, and rotation."
os.clean_transforms[prosperon.DOC] = "Force an update on all transforms to remove dangling references or perform house-keeping."
os.platform[prosperon.DOC] = "Return a string with the underlying platform name, like 'Windows', 'Linux', or 'macOS'." os.platform[prosperon.DOC] = "Return a string with the underlying platform name, like 'Windows', 'Linux', or 'macOS'."
os.arch[prosperon.DOC] = "Return the CPU architecture string for this system (e.g. 'x64', 'arm64')." os.arch[prosperon.DOC] = "Return the CPU architecture string for this system (e.g. 'x64', 'arm64')."
os.totalmem[prosperon.DOC] = "Return the total system RAM in bytes." os.totalmem[prosperon.DOC] = "Return the total system RAM in bytes."
@@ -10,25 +7,13 @@ os.freemem[prosperon.DOC] = "Return the amount of free system RAM in bytes, if k
os.hostname[prosperon.DOC] = "Return the system's hostname, or an empty string if not available." os.hostname[prosperon.DOC] = "Return the system's hostname, or an empty string if not available."
os.version[prosperon.DOC] = "Return the OS or kernel version string, if the platform provides it." os.version[prosperon.DOC] = "Return the OS or kernel version string, if the platform provides it."
os.kill[prosperon.DOC] = "Send a signal (e.g., 'SIGINT', 'SIGTERM', etc.) to the current process."
os.exit[prosperon.DOC] = "Exit the application with the specified exit code." os.exit[prosperon.DOC] = "Exit the application with the specified exit code."
os.now[prosperon.DOC] = "Return current time (in seconds as a float) with high resolution." os.now[prosperon.DOC] = "Return current time (in seconds as a float) with high resolution."
os.openurl[prosperon.DOC] = "Open the provided URL in the default web browser, if possible."
os.make_timer[prosperon.DOC] = "Create a new timer object that will call a specified function after a certain delay."
os.update_timers[prosperon.DOC] = "Advance all timers by the provided time delta (in seconds)."
os.sleep[prosperon.DOC] = "Block execution for the specified number of seconds."
os.battery_pct[prosperon.DOC] = "Return the battery level (percentage) or negative if unknown."
os.battery_voltage[prosperon.DOC] = "Return the current battery voltage in volts, if available."
os.battery_seconds[prosperon.DOC] = "Return the estimated remaining battery time in seconds, or negative if unknown."
os.power_state[prosperon.DOC] = "Return a string describing power status: 'on battery', 'charging', 'charged', etc." os.power_state[prosperon.DOC] = "Return a string describing power status: 'on battery', 'charging', 'charged', etc."
os.on[prosperon.DOC] = "Register a global callback for certain engine-wide or system-level events." os.on[prosperon.DOC] = "Register a global callback for certain engine-wide or system-level events."
os.rt_info[prosperon.DOC] = "Return internal QuickJS runtime info, such as object counts."
os.rusage[prosperon.DOC] = "Return resource usage stats for this process, if the platform supports it." os.rusage[prosperon.DOC] = "Return resource usage stats for this process, if the platform supports it."
os.mallinfo[prosperon.DOC] = "Return detailed memory allocation info (arena size, free blocks, etc.) on some platforms." os.mallinfo[prosperon.DOC] = "Return detailed memory allocation info (arena size, free blocks, etc.) on some platforms."
os.env[prosperon.DOC] = "Fetch the value of a given environment variable, or undefined if it doesn't exist."
os.system[prosperon.DOC] = "Execute a shell command using the system() call. Returns the command's exit code."
return os return os

584
scripts/modules/parseq.js Normal file
View File

@@ -0,0 +1,584 @@
// parseq.js
// Douglas Crockford
// 2020-11-09
// Better living thru eventuality!
// You can access the parseq object in your module by importing it.
// import parseq from "./parseq.js";
/*jslint node */
/*property
concat, create, evidence, fallback, forEach, freeze, isArray, isSafeInteger,
keys, length, min, parallel, parallel_object, pop, push, race, sequence,
some
*/
function make_reason(factory_name, excuse, evidence) {
// Make a reason object. These are used for exceptions and cancellations.
// They are made from Error objects.
const reason = new Error("parseq." + factory_name + (
excuse === undefined
? ""
: ": " + excuse
));
reason.evidence = evidence;
return reason;
}
function get_array_length(array, factory_name) {
if (Array.isArray(array)) {
return array.length;
}
if (array === undefined) {
return 0;
}
throw make_reason(factory_name, "Not an array.", array);
}
function check_callback(callback, factory_name) {
if (typeof callback !== "function" || callback.length !== 2) {
throw make_reason(factory_name, "Not a callback function.", callback);
}
}
function check_requestors(requestor_array, factory_name) {
// A requestor array contains only requestors. A requestor is a function that
// takes wun or two arguments: 'callback' and optionally 'initial_value'.
if (requestor_array.some(function (requestor) {
return (
typeof requestor !== "function"
|| requestor.length < 1
|| requestor.length > 2
);
})) {
throw make_reason(
factory_name,
"Bad requestors array.",
requestor_array
);
}
}
function run(
factory_name,
requestor_array,
initial_value,
action,
timeout,
time_limit,
throttle = 0
) {
// The 'run' function does the work that is common to all of the Parseq
// factories. It takes the name of the factory, an array of requestors, an
// initial value, an action callback, a timeout callback, a time limit in
// milliseconds, and a throttle.
// If all goes well, we call all of the requestor functions in the array. Each
// of them might return a cancel function that is kept in the 'cancel_array'.
let cancel_array = new Array(requestor_array.length);
let next_number = 0;
let timer_id;
// We need 'cancel' and 'start_requestor' functions.
function cancel(reason = make_reason(factory_name, "Cancel.")) {
// Stop all unfinished business. This can be called when a requestor fails.
// It can also be called when a requestor succeeds, such as 'race' stopping
// its losers, or 'parallel' stopping the unfinished optionals.
// If a timer is running, stop it.
if (timer_id !== undefined) {
clearTimeout(timer_id);
timer_id = undefined;
}
// If anything is still going, cancel it.
if (cancel_array !== undefined) {
cancel_array.forEach(function (cancel) {
try {
if (typeof cancel === "function") {
return cancel(reason);
}
} catch (ignore) {}
});
cancel_array = undefined;
}
}
function start_requestor(value) {
// The 'start_requestor' function is not recursive, exactly. It does not
// directly call itself, but it does return a function that might call
// 'start_requestor'.
// Start the execution of a requestor, if there are any still waiting.
if (
cancel_array !== undefined
&& next_number < requestor_array.length
) {
// Each requestor has a number.
let number = next_number;
next_number += 1;
// Call the next requestor, passing in a callback function,
// saving the cancel function that the requestor might return.
const requestor = requestor_array[number];
try {
cancel_array[number] = requestor(
function start_requestor_callback(value, reason) {
// This callback function is called by the 'requestor' when it is done.
// If we are no longer running, then this call is ignored.
// For example, it might be a result that is sent back after the time
// limit has expired. This callback function can only be called wunce.
if (
cancel_array !== undefined
&& number !== undefined
) {
// We no longer need the cancel associated with this requestor.
cancel_array[number] = undefined;
// Call the 'action' function to let the requestor know what happened.
action(value, reason, number);
// Clear 'number' so this callback can not be used again.
number = undefined;
// If there are any requestors that are still waiting to start, then
// start the next wun. If the next requestor is in a sequence, then it
// gets the most recent 'value'. The others get the 'initial_value'.
setTimeout(start_requestor, 0, (
factory_name === "sequence"
? value
: initial_value
));
}
},
value
);
// Requestors are required to report their failure thru the callback.
// They are not allowed to throw exceptions. If we happen to catch wun,
// it is treated as a failure.
} catch (exception) {
action(undefined, exception, number);
number = undefined;
start_requestor(value);
}
}
}
// With the 'cancel' and the 'start_requestor' functions in hand,
// we can now get to work.
// If a timeout was requested, start the timer.
if (time_limit !== undefined) {
if (typeof time_limit === "number" && time_limit >= 0) {
if (time_limit > 0) {
timer_id = setTimeout(timeout, time_limit);
}
} else {
throw make_reason(factory_name, "Bad time limit.", time_limit);
}
}
// If we are doing 'race' or 'parallel', we want to start all of the requestors
// at wunce. However, if there is a 'throttle' in place then we start as many
// as the 'throttle' allows, and then as each requestor finishes, another is
// started.
// The 'sequence' and 'fallback' factories set 'throttle' to 1 because they
// process wun at a time and always start another requestor when the
// previous requestor finishes.
if (!Number.isSafeInteger(throttle) || throttle < 0) {
throw make_reason(factory_name, "Bad throttle.", throttle);
}
let repeat = Math.min(throttle || Infinity, requestor_array.length);
while (repeat > 0) {
setTimeout(start_requestor, 0, initial_value);
repeat -= 1;
}
// We return 'cancel' which allows the requestor to cancel this work.
return cancel;
}
// The factories ///////////////////////////////////////////////////////////////
function parallel(
required_array,
optional_array,
time_limit,
time_option,
throttle,
factory_name = "parallel"
) {
// The parallel factory is the most complex of these factories. It can take
// a second array of requestors that get a more forgiving failure policy.
// It returns a requestor that produces an array of values.
let requestor_array;
// There are four cases because 'required_array' and 'optional_array'
// can both be empty.
let number_of_required = get_array_length(required_array, factory_name);
if (number_of_required === 0) {
if (get_array_length(optional_array, factory_name) === 0) {
// If both are empty, then 'requestor_array' is empty.
requestor_array = [];
} else {
// If there is only 'optional_array', then it is the 'requestor_array'.
requestor_array = optional_array;
time_option = true;
}
} else {
// If there is only 'required_array', then it is the 'requestor_array'.
if (get_array_length(optional_array, factory_name) === 0) {
requestor_array = required_array;
time_option = undefined;
// If both arrays are provided, we concatenate them together.
} else {
requestor_array = required_array.concat(optional_array);
if (time_option !== undefined && typeof time_option !== "boolean") {
throw make_reason(
factory_name,
"Bad time_option.",
time_option
);
}
}
}
// We check the array and return the requestor.
check_requestors(requestor_array, factory_name);
return function parallel_requestor(callback, initial_value) {
check_callback(callback, factory_name);
let number_of_pending = requestor_array.length;
let number_of_pending_required = number_of_required;
let results = [];
if (number_of_pending === 0) {
callback(
factory_name === "sequence"
? initial_value
: results
);
return;
}
// 'run' gets it started.
let cancel = run(
factory_name,
requestor_array,
initial_value,
function parallel_action(value, reason, number) {
// The action function gets the result of each requestor in the array.
// 'parallel' wants to return an array of all of the values it sees.
results[number] = value;
number_of_pending -= 1;
// If the requestor was wun of the requireds, make sure it was successful.
// If it failed, then the parallel operation fails. If an optionals requestor
// fails, we can still continue.
if (number < number_of_required) {
number_of_pending_required -= 1;
if (value === undefined) {
cancel(reason);
callback(undefined, reason);
callback = undefined;
return;
}
}
// If all have been processed, or if the requireds have all succeeded
// and we do not have a 'time_option', then we are done.
if (
number_of_pending < 1
|| (
time_option === undefined
&& number_of_pending_required < 1
)
) {
cancel(make_reason(factory_name, "Optional."));
callback(
factory_name === "sequence"
? results.pop()
: results
);
callback = undefined;
}
},
function parallel_timeout() {
// When the timer fires, work stops unless we were under the 'false'
// time option. The 'false' time option puts no time limits on the
// requireds, allowing the optionals to run until the requireds finish
// or the time expires, whichever happens last.
const reason = make_reason(
factory_name,
"Timeout.",
time_limit
);
if (time_option === false) {
time_option = undefined;
if (number_of_pending_required < 1) {
cancel(reason);
callback(results);
}
} else {
// Time has expired. If all of the requireds were successful,
// then the parallel operation is successful.
cancel(reason);
if (number_of_pending_required < 1) {
callback(results);
} else {
callback(undefined, reason);
}
callback = undefined;
}
},
time_limit,
throttle
);
return cancel;
};
}
function parallel_object(
required_object,
optional_object,
time_limit,
time_option,
throttle
) {
// 'parallel_object' is similar to 'parallel' except that it takes and
// produces objects of requestors instead of arrays of requestors. This
// factory converts the objects to arrays, and the requestor it returns
// turns them back again. It lets 'parallel' do most of the work.
const names = [];
let required_array = [];
let optional_array = [];
// Extract the names and requestors from 'required_object'.
// We only collect functions with an arity of 1 or 2.
if (required_object) {
if (typeof required_object !== "object") {
throw make_reason(
"parallel_object",
"Type mismatch.",
required_object
);
}
Object.keys(required_object).forEach(function (name) {
let requestor = required_object[name];
if (
typeof requestor === "function"
&& (requestor.length === 1 || requestor.length === 2)
) {
names.push(name);
required_array.push(requestor);
}
});
}
// Extract the names and requestors from 'optional_object'.
// Look for duplicate keys.
if (optional_object) {
if (typeof optional_object !== "object") {
throw make_reason(
"parallel_object",
"Type mismatch.",
optional_object
);
}
Object.keys(optional_object).forEach(function (name) {
let requestor = optional_object[name];
if (
typeof requestor === "function"
&& (requestor.length === 1 || requestor.length === 2)
) {
if (required_object && required_object[name] !== undefined) {
throw make_reason(
"parallel_object",
"Duplicate name.",
name
);
}
names.push(name);
optional_array.push(requestor);
}
});
}
// Call 'parallel' to get a requestor.
const parallel_requestor = parallel(
required_array,
optional_array,
time_limit,
time_option,
throttle,
"parallel_object"
);
// Return the parallel object requestor.
return function parallel_object_requestor(callback, initial_value) {
// When our requestor is called, we return the result of our parallel requestor.
return parallel_requestor(
// We pass our callback to the parallel requestor,
// converting its value into an object.
function parallel_object_callback(value, reason) {
if (value === undefined) {
return callback(undefined, reason);
}
const object = Object.create(null);
names.forEach(function (name, index) {
object[name] = value[index];
});
return callback(object);
},
initial_value
);
};
}
function race(requestor_array, time_limit, throttle) {
// The 'race' factory returns a requestor that starts all of the
// requestors in 'requestor_array' at wunce. The first success wins.
const factory_name = (
throttle === 1
? "fallback"
: "race"
);
if (get_array_length(requestor_array, factory_name) === 0) {
throw make_reason(factory_name, "No requestors.");
}
check_requestors(requestor_array, factory_name);
return function race_requestor(callback, initial_value) {
check_callback(callback, factory_name);
let number_of_pending = requestor_array.length;
let cancel = run(
factory_name,
requestor_array,
initial_value,
function race_action(value, reason, number) {
number_of_pending -= 1;
if (value !== undefined) {
// We have a winner. Cancel the losers and pass the value to the 'callback'.
cancel(make_reason(factory_name, "Loser.", number));
callback(value);
callback = undefined;
} else if (number_of_pending < 1) {
// There was no winner. Signal a failure.
cancel(reason);
callback(undefined, reason);
callback = undefined;
}
},
function race_timeout() {
let reason = make_reason(
factory_name,
"Timeout.",
time_limit
);
cancel(reason);
callback(undefined, reason);
callback = undefined;
},
time_limit,
throttle
);
return cancel;
};
}
function fallback(requestor_array, time_limit) {
// The 'fallback' factory returns a requestor that tries each requestor
// in 'requestor_array', wun at a time, until it finds a successful wun.
return race(requestor_array, time_limit, 1);
}
function sequence(requestor_array, time_limit) {
// A sequence runs each requestor in order, passing results to the next,
// as long as they are all successful. A sequence is a throttled parallel.
return parallel(
requestor_array,
undefined,
time_limit,
undefined,
1,
"sequence"
);
}
return {
fallback,
parallel,
parallel_object,
race,
sequence
}

View File

@@ -1,856 +1 @@
var render = {} return use('sdl_render')
var io = use('io')
var os = use('os')
var controller = use('controller')
var tracy = use('tracy')
var graphics = use('graphics')
var default_conf = {
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
width: 1280,
height: 720,
icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
high_dpi:0,
alpha:1,
fullscreen:0,
sample_count:1,
enable_clipboard:true,
enable_dragndrop: true,
max_dropped_files: 1,
swap_interval: 1,
name: "Prosperon",
version:prosperon.version + "-" + prosperon.revision,
identifier: "world.pockle.prosperon",
creator: "Pockle World LLC",
copyright: "Copyright Pockle World 2025",
type: "application",
url: "https://github.com/johnbrethauer/prosperon"
}
var config = use('config.js')
config.__proto__ = default_conf
prosperon.camera = use('camera').make()
prosperon.camera.size = [config.width,config.height]
var base_pipeline = {
vertex: "sprite.vert",
fragment: "sprite.frag",
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
fill: true, // false for lines
depth: {
compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
test: false,
write: false,
bias: 0,
bias_slope_scale: 0,
bias_clamp: 0
},
stencil: {
enabled: true,
front: {
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
depth_fail: "keep",
pass: "keep"
},
back: {
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
depth_fail: "keep",
pass: "keep"
},
test: true,
compare_mask: 0,
write_mask: 0
},
blend: {
enabled: false,
src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
dst_rgb: "zero",
op_rgb: "add", // add/sub/rev_sub/min/max
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
},
cull: "none", // none/front/back
face: "cw", // cw/ccw
alpha_to_coverage: false,
multisample: {
count: 1, // number of multisamples
mask: 0xFFFFFFFF,
domask: false
},
label: "scripted pipeline",
target: {}
}
var sprite_pipeline = Object.create(base_pipeline);
sprite_pipeline.blend = {
enabled:true,
src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
dst_rgb: "one_minus_src_alpha",
op_rgb: "add", // add/sub/rev_sub/min/max
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
};
sprite_pipeline.target = {
color_targets: [{
format:"rgba8",
blend:sprite_pipeline.blend
}],
depth: "d32 float s8"
};
var appy = {};
appy.inputs = {};
if (os.platform() === "macos") {
appy.inputs["S-q"] = os.exit;
}
appy.inputs["M-f4"] = os.exit;
controller.player[0].control(appy);
prosperon.window = prosperon.engine_start(config);
var driver = "vulkan"
switch(os.platform()) {
case "Linux":
driver = "vulkan"
break
case "Windows":
// driver = "direct3d12"
driver = "vulkan"
break
case "macOS":
driver = "metal"
break
}
render._main = prosperon.window.make_gpu(false,driver)
prosperon.gpu = render._main
render._main.window = prosperon.window
render._main.claim_window(prosperon.window)
render._main.set_swapchain('sdr', 'vsync')
var unit_transform = os.make_transform();
var cur = {};
cur.images = [];
cur.samplers = [];
var tbuffer;
function full_upload(buffers) {
var cmds = render._main.acquire_cmd_buffer();
tbuffer = render._main.upload(cmds, buffers, tbuffer);
cmds.submit();
}
full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit.
:param buffers: An array of data buffers to be uploaded.
:return: None
`
function bind_pipeline(pass, pipeline) {
make_pipeline(pipeline)
pass.bind_pipeline(pipeline.gpu)
pass.pipeline = pipeline;
}
bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass.
:param pass: The current render pass to bind the pipeline to.
:param pipeline: The pipeline object containing shader and state info.
:return: None
`
var main_pass;
var cornflower = [62/255,96/255,113/255,1];
function get_pipeline_ubo_slot(pipeline, name) {
if (!pipeline.vertex.reflection.ubos) return;
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
var ubo = pipeline.vertex.reflection.ubos[i];
if (ubo.name.endsWith(name))
return i;
}
return undefined;
}
get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix.
:param pipeline: The pipeline whose vertex reflection is inspected.
:param name: A string suffix to match against the uniform buffer block name.
:return: The integer index of the matching UBO, or undefined if not found.
`
function transpose4x4(val) {
var out = [];
out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12];
out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13];
out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
return out;
}
transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix.
:param val: An array of length 16 representing a 4x4 matrix in row-major format.
:return: A new array of length 16 representing the transposed matrix.
`
function ubo_obj_to_array(pipeline, name, obj) {
var ubo;
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
ubo = pipeline.vertex.reflection.ubos[i];
if (ubo.name.endsWith(name)) break;
}
var type = pipeline.vertex.reflection.types[ubo.type];
var len = 0;
for (var mem of type.members)
len += type_to_byte_count(mem.type);
var buf = new ArrayBuffer(len);
var view = new DataView(buf);
for (var mem of type.members) {
var val = obj[mem.name];
if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);
if (mem.name === 'model')
val = transpose4x4(val.array());
for (var i = 0; i < val.length; i++)
view.setFloat32(mem.offset + i*4, val[i],true);
}
return buf;
}
ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info.
:param pipeline: The pipeline whose vertex reflection is read for UBO structure.
:param name: The name suffix that identifies the target UBO in the reflection data.
:param obj: An object whose properties match the UBO members.
:return: An ArrayBuffer containing packed UBO data.
`
function type_to_byte_count(type) {
switch (type) {
case 'float': return 4;
case 'vec2': return 8;
case 'vec3': return 12;
case 'vec4': return 16;
case 'mat4': return 64;
default: throw new Error("Unknown or unsupported float-based type: " + type);
}
}
type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types.
:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4').
:return: Integer number of bytes.
`
var sprite_model_ubo = {
model: unit_transform,
color: [1,1,1,1]
};
var shader_cache = {};
var shader_times = {};
function make_pipeline(pipeline) {
if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made
if (typeof pipeline.vertex === 'string')
pipeline.vertex = make_shader(pipeline.vertex);
if (typeof pipeline.fragment === 'string')
pipeline.fragment = make_shader(pipeline.fragment)
// 1) Reflection data for vertex shader
var refl = pipeline.vertex.reflection
if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
pipeline.gpu = render._main.make_pipeline(pipeline);
return;
}
var inputs = refl.inputs
var buffer_descriptions = []
var attributes = []
// 2) Build buffer + attribute for each reflection input
for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i]
var typeStr = inp.type
var nameStr = (inp.name || "").toUpperCase()
var pitch = 4
var fmt = "float1"
if (typeStr == "vec2") {
pitch = 8
fmt = "float2"
} else if (typeStr == "vec3") {
pitch = 12
fmt = "float3"
} else if (typeStr == "vec4") {
if (nameStr.indexOf("COLOR") >= 0) {
pitch = 16
fmt = "color"
} else {
pitch = 16
fmt = "float4"
}
}
buffer_descriptions.push({
slot: i,
pitch: pitch,
input_rate: "vertex",
instance_step_rate: 0,
name:inp.name.split(".").pop()
})
attributes.push({
location: inp.location,
buffer_slot: i,
format: fmt,
offset: 0
})
}
pipeline.vertex_buffer_descriptions = buffer_descriptions
pipeline.vertex_attributes = attributes
pipeline.gpu = render._main.make_pipeline(pipeline);
}
make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created.
:param pipeline: An object describing the pipeline state, shaders, and reflection data.
:return: None
`
var shader_type;
function make_shader(sh_file) {
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
if (shader_cache[file]) return shader_cache[file]
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
var shader = {
code: io.slurpbytes(file),
format: shader_type,
stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
num_textures: 0,
num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
entrypoint: shader_type === "msl" ? "main0" : "main"
}
console.log(`making shader ${sh_file} of format ${shader_type}`)
shader.gpu = render._main.make_shader(shader)
shader.reflection = refl;
shader_cache[file] = shader
shader.file = sh_file
return shader
}
make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded.
:param sh_file: The base filename (without extension) of the shader to compile.
:return: A shader object with GPU and reflection data attached.
`
// helpful render devices. width and height in pixels; diagonal in inches.
render.device = {
pc: { width: 1920, height: 1080 },
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
ds_top: { width: 400, height: 240, diagonal: 3.53 },
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
playdate: { width: 400, height: 240, diagonal: 2.7 },
switch: { width: 1280, height: 720, diagonal: 6.2 },
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
switch_oled: { width: 1280, height: 720, diagonal: 7 },
dsi: { width: 256, height: 192, diagonal: 3.268 },
ds: { width: 256, height: 192, diagonal: 3 },
dsixl: { width: 256, height: 192, diagonal: 4.2 },
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
gba: { width: 240, height: 160, diagonal: 2.9 },
gameboy: { width: 160, height: 144, diagonal: 2.48 },
gbc: { width: 160, height: 144, diagonal: 2.28 },
steamdeck: { width: 1280, height: 800, diagonal: 7 },
vita: { width: 960, height: 544, diagonal: 5 },
psp: { width: 480, height: 272, diagonal: 4.3 },
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
ps1: { width: 320, height: 240, diagonal: 5 },
ps2: { width: 640, height: 480 },
snes: { width: 256, height: 224 },
gamecube: { width: 640, height: 480 },
n64: { width: 320, height: 240 },
c64: { width: 320, height: 200 },
macintosh: { width: 512, height: 342 },
gamegear: { width: 160, height: 144, diagonal: 3.2 }
};
render.device.doc = `Device resolutions given as [x,y,inches diagonal].`;
var render_queue = [];
var hud_queue = [];
var current_queue = render_queue;
var std_sampler = {
min_filter: "nearest",
mag_filter: "nearest",
mipmap: "linear",
u: "repeat",
v: "repeat",
w: "repeat",
mip_bias: 0,
max_anisotropy: 0,
compare_op: "none",
min_lod: 0,
max_lod: 0,
anisotropy: false,
compare: false
};
function upload_model(model) {
var bufs = [];
for (var i in model) {
if (typeof model[i] !== 'object') continue;
bufs.push(model[i]);
}
render._main.upload(this, bufs);
}
upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU.
:param model: An object whose buffer properties are to be uploaded.
:return: None
`
function bind_model(pass, pipeline, model) {
var buffers = pipeline.vertex_buffer_descriptions;
var bufs = [];
if (buffers)
for (var b of buffers) {
if (b.name in model) bufs.push(model[b.name])
else throw Error (`could not find buffer ${b.name} on model`);
}
pass.bind_buffers(0,bufs);
pass.bind_index_buffer(model.indices);
}
bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass.
:param pass: The current render pass.
:param pipeline: The pipeline object with vertex buffer descriptions.
:param model: The model object containing matching buffers and an index buffer.
:return: None
`
function bind_mat(pass, pipeline, mat) {
var imgs = [];
var refl = pipeline.fragment.reflection;
if (refl.separate_images) {
for (var i of refl.separate_images) {
if (i.name in mat) {
var tex = mat[i.name];
imgs.push({texture:tex.texture, sampler:tex.sampler});
} else
throw Error (`could not find all necessary images: ${i.name}`)
}
pass.bind_samplers(false, 0,imgs);
}
}
bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader.
:param pass: The current render pass.
:param pipeline: The pipeline whose fragment shader reflection indicates required textures.
:param mat: An object mapping the required image names to {texture, sampler}.
:return: None
`
function group_sprites_by_texture(sprites, mesh) {
if (sprites.length === 0) return;
for (var i = 0; i < sprites.length; i++) {
sprites[i].mesh = mesh;
sprites[i].first_index = i*6;
sprites[i].num_indices = 6;
}
return;
// The code below is an alternate approach to grouping by image. Currently not in use.
/*
var groups = [];
var group = {image:sprites[0].image, first_index:0};
var count = 1;
for (var i = 1; i < sprites.length; i++) {
if (sprites[i].image === group.image) {
count++;
continue;
}
group.num_indices = count*6;
var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
group = newgroup;
groups.push(group);
count=1;
}
group.num_indices = count*6;
return groups;
*/
}
group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed.
:param sprites: An array of sprite objects.
:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite.
:return: None
`
var main_color = {
type:"2d",
format: "rgba8",
layers: 1,
mip_levels: 1,
samples: 0,
sampler:true,
color_target:true
};
var main_depth = {
type: "2d",
format: "d32 float s8",
layers:1,
mip_levels:1,
samples:0,
sampler:true,
depth_target:true
};
function render_camera(cmds, camera) {
var pass;
delete camera.target // TODO: HORRIBLE
if (!camera.target) {
main_color.width = main_depth.width = camera.size.x;
main_color.height = main_depth.height = camera.size.y;
camera.target = {
color_targets: [{
texture: render._main.texture(main_color),
mip_level:0,
layer: 0,
load:"clear",
store:"store",
clear: cornflower
}],
depth_stencil: {
texture: render._main.texture(main_depth),
clear:1,
load:"dont_care",
store:"dont_care",
stencil_load:"dont_care",
stencil_store:"dont_care",
stencil_clear:0
}
};
}
var buffers = [];
buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue));
var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
for (var q of unique_meshes)
buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]);
buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue));
for (var q of hud_queue)
if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]);
full_upload(buffers)
var pass = cmds.render_pass(camera.target);
var pipeline = sprite_pipeline;
bind_pipeline(pass,pipeline);
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.camera(camera, camslot);
modelslot = get_pipeline_ubo_slot(pipeline, "model");
if (typeof modelslot !== 'undefined') {
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
cmds.push_vertex_uniform_data(modelslot, ubo);
}
var mesh;
var img;
var modelslot;
cmds.push_debug_group("draw")
for (var group of render_queue) {
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group()
cmds.push_debug_group("hud")
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.hud(camera.size, camslot);
for (var group of hud_queue) {
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group();
pass?.end();
render_queue = [];
hud_queue = [];
}
render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items.
:param cmds: A command buffer obtained from the GPU context.
:param camera: The camera object (with size, optional target, etc.).
:return: None
`
var imgui = use('imgui')
if (imgui) imgui.init(render._main, prosperon.window);
var swaps = [];
function gpupresent() {
os.clean_transforms();
var cmds = render._main.acquire_cmd_buffer();
render_camera(cmds, prosperon.camera);
var swapchain_tex = cmds.acquire_swapchain();
if (!swapchain_tex)
cmds.cancel();
else {
var torect = prosperon.camera.draw_rect(prosperon.window.size);
torect.texture = swapchain_tex;
if (swapchain_tex) {
cmds.blit({
src: prosperon.camera.target.color_targets[0].texture,
dst: torect,
filter:"nearest",
load: "clear"
});
if (imgui) {
cmds.push_debug_group("imgui")
imgui.prepend(cmds);
var pass = cmds.render_pass({
color_targets:[{texture:swapchain_tex}]});
imgui.endframe(cmds,pass);
pass.end();
cmds.pop_debug_group()
}
}
cmds.submit()
}
}
gpupresent[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available.
:return: None
`
var stencil_write = {
compare: "always",
fail_op: "replace",
depth_fail_op: "replace",
pass_op: "replace"
};
var stencil_writer = function stencil_writer(ref) {
var pipe = Object.create(base_pipeline);
Object.assign(pipe, {
stencil: {
enabled: true,
front: stencil_write,
back: stencil_write,
write:true,
read:true,
ref:ref
},
write_mask: colormask.none
});
return pipe;
}.hashify();
render.stencil_writer = stencil_writer;
// objects by default draw where the stencil buffer is 0
render.fillmask = function fillmask(ref) {
var pipe = stencil_writer(ref);
render.use_shader('screenfill.cg', pipe);
render.draw(shape.quad);
}
render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.
:param ref: The stencil reference value to write.
:return: None
`
var stencil_invert = {
compare: "always",
fail_op: "invert",
depth_fail_op: "invert",
pass_op: "invert"
};
render.mask = function mask(image, pos, scale, rotation = 0, ref = 1) {
if (typeof image === 'string')
image = graphics.texture(image);
var tex = image.texture;
if (scale) scale = scale.div([tex.width,tex.height]);
else scale = [1,1,1]
var pipe = stencil_writer(ref);
render.use_shader('sprite.cg', pipe);
var t = os.make_transform();
t.trs(pos, undefined, scale);
set_model(t);
render.use_mat({
diffuse:image.texture,
rect: image.rect,
shade: Color.white
});
render.draw(shape.quad);
}
render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value.
:param image: A texture or string path (which is converted to a texture).
:param pos: The translation (x, y) for the image placement.
:param scale: Optional scaling applied to the texture.
:param rotation: Optional rotation in radians (unused by default).
:param ref: The stencil reference value to write.
:return: None
`
render.viewport = function(rect) {
render._main.viewport(rect);
}
render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle.
:param rect: A rectangle [x, y, width, height].
:return: None
`
render.scissor = function(rect) {
render.viewport(rect)
}
render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport).
:param rect: A rectangle [x, y, width, height].
:return: None
`
var imdebug = function imdebug() {
imtoggle("Physics", debug, "draw_phys");
imtoggle("Bouning boxes", debug, "draw_bb");
imtoggle("Names", debug, "draw_names");
imtoggle("Sprite nums", debug, "sprite_nums");
imtoggle("Debug overlay", debug, "show");
imtoggle("Show ur names", debug, "urnames");
};
var observed_tex = undefined;
var debug = {}
debug.console = false
// Some initialization
shader_type = render._main.shader_format()[0];
std_sampler = render._main.make_sampler({
min_filter: "nearest",
mag_filter: "nearest",
mipmap_mode: "nearest",
address_mode_u: "repeat",
address_mode_v: "repeat",
address_mode_w: "repeat"
});
render._main.present = gpupresent;
if (tracy) tracy.gpu_init()
render.queue = function(cmd) {
if (Array.isArray(cmd))
for (var i of cmd) current_queue.push(i)
else
current_queue.push(cmd)
}
render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called.
:param cmd: Either a single command object or an array of command objects.
:return: None
`
render.setup_draw = function() {
current_queue = render_queue;
prosperon.draw();
}
render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.
:return: None
`
render.setup_hud = function() {
current_queue = hud_queue;
prosperon.hud();
}
render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.
:return: None
`
return render

View File

@@ -1,6 +1,19 @@
var io = use_embed('io') var io = use_embed('io');
var miniz = use_embed('miniz') var miniz = use_embed('miniz');
var os = use_embed('os') var os = use_embed('os');
Object.defineProperty(Function.prototype, "hashify", {
value: function () {
var hash = new Map()
var fn = this
function hashified(...args) {
var key = args[0]
if (!hash.has(key)) hash.set(key, fn(...args))
return hash.get(key)
}
return hashified
},
})
// Merge of the old resources.js and packer.js functionalities // Merge of the old resources.js and packer.js functionalities
var Resources = {} var Resources = {}
@@ -41,9 +54,9 @@ function isRecognizedExtension(ext) {
return false return false
} }
// Attempt to find file with or without extension from the current PATH
// (From the original resources.js, unchanged except for code style)
function find_in_path(filename, exts = []) { function find_in_path(filename, exts = []) {
if (typeof filename !== 'string') return undefined
if (filename.includes('.')) { if (filename.includes('.')) {
for (var dir of prosperon.PATH) { for (var dir of prosperon.PATH) {
var candidate = dir + filename var candidate = dir + filename
@@ -53,10 +66,15 @@ function find_in_path(filename, exts = []) {
} }
for (var dir of prosperon.PATH) { for (var dir of prosperon.PATH) {
var candidate = dir + filename // Only check extensions if exts is provided and not empty
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate if (exts.length > 0) {
for (var ext of exts) { for (var ext of exts) {
candidate = dir + filename + '.' + ext var candidate = dir + filename + '.' + ext
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
}
} else {
// Fallback to extensionless file only if no extensions are specified
var candidate = dir + filename
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
} }
} }

View File

@@ -0,0 +1,397 @@
var ex = this
var input = use('input')
var DEAD = Symbol()
var GARBAGE = Symbol()
var FILE = Symbol()
var TIMERS = Symbol()
var REGGIES = Symbol()
var UNDERLINGS = Symbol()
var OVERLING = Symbol()
function add_timer(obj, fn, seconds) {
var timers = obj[TIMERS]
var stop = function () {
if (!timer) return
timers.delete(stop)
timer.fn = undefined
timer = undefined
}
function execute() {
if (fn) timer.remain = fn(stop.seconds)
if (!timer) return
if (!timer.remain) stop()
else stop.seconds = timer.remain
}
// var timer = os.make_timer(execute)
timer.remain = seconds
stop.remain = seconds
stop.seconds = seconds
timers.push(stop)
return stop
}
globalThis.Register = {
registries: [],
add_cb(name) {
var n = {}
var fns = []
n.register = function (fn, oname) {
if (typeof fn !== 'function') return
var dofn = function (...args) {
fn(...args)
}
Object.defineProperty(dofn, 'name', {value:`do_${oname}`})
var left = 0
var right = fns.length - 1
dofn.layer = fn.layer
dofn.layer ??= 0
while (left <= right) {
var mid = Math.floor((left + right) / 2)
if (fns[mid] === dofn.layer) {
left = mid
break
} else if (fns[mid].layer < dofn.layer) left = mid + 1
else right = mid - 1
}
fns.splice(left, 0, dofn)
return function () {
fns.delete(dofn)
}
}
prosperon[name] = function (...args) {
fns.forEach(fn => {
fn(...args)
})
}
Object.defineProperty(prosperon[name], 'name', {value:name})
prosperon[name].fns = fns
n.clear = function () {
fns = []
}
Register[name] = n
Register.registries[name] = n
return n
},
}
Register.pull_registers = function pull_registers(obj) {
var reggies = []
for (var reg in Register.registries) {
if (typeof obj[reg] === "function")
reggies.push(reg)
}
return reggies
}
Register.register_obj = function register_obj(obj, reg) {
var fn = obj[reg].bind(obj)
fn.layer = obj[reg].layer
var name = obj.ur ? obj.ur.name : obj.toString()
obj[TIMERS].push(Register.registries[reg].register(fn, name))
if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`})
}
Register.check_registers = function check_registers(obj) {
if (obj[REGGIES]) {
if (obj[REGGIES].length == 0) return
for (var reg of obj[REGGIES])
Register.register_obj(obj,reg)
return
}
for (var reg in Register.registries) {
if (typeof obj[reg] === "function")
Register.register_obj(obj,reg)
}
}
Register.add_cb("appupdate")
Register.add_cb("update").doc = "Called once per frame."
Register.add_cb("physupdate")
Register.add_cb("gui")
Register.add_cb("hud")
Register.add_cb("draw")
Register.add_cb("imgui")
Register.add_cb("app")
var actor = {}
actor.toString = function() { return this[FILE] }
actor.spawn = function spawn(script, config, actor_context) {
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
var prog
if (!script) {
prog = {}
prog.module_ret = {}
prog.prog_fn = function() {}
} else {
prog = script_fn(script)
if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
}
var underling
prog.module_ret.__proto__ = actor
underling = Object.create(prog.module_ret)
underling[OVERLING] = this
underling[FILE] = script
underling[TIMERS] = []
underling[UNDERLINGS] = new Set()
// Make $_ available to the actor (either passed context or the engine's $_)
var actor_dollar = actor_context || $_
Object.defineProperty(underling, '$_', {
value: actor_dollar,
writable:false,
enumerable:false,
configurable:false
})
Object.defineProperty(underling, 'overling', {
get() { return this[OVERLING] },
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'underlings', {
get() { return new Set(this[UNDERLINGS]) },
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'spawn', {
value: function(script, config) {
return actor.spawn.call(this, script, config, actor_dollar)
},
writable:false,
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'kill', {
value: actor.kill,
writable:false,
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'delay', {
value: actor.delay,
writable:false,
enumerable:true,
configurable:false
})
try {
// Pass $_ as a parameter to actor scripts
prog.prog_fn.call(underling, actor_dollar)
} catch(e) { throw e; }
if (underling[DEAD]) return undefined;
if (typeof config === 'object') Object.assign(underling, config)
if (!underling[REGGIES])
underling.__proto__[REGGIES] = Register.pull_registers(underling)
Register.check_registers(underling)
if (underling.awake) underling.awake()
this[UNDERLINGS].add(underling)
if (underling.tag) act.tag_add(underling.tag, underling)
underling[GARBAGE] = underling.garbage
return underling
}
actor.clear = function actor_clear() {
this[UNDERLINGS].forEach(p => {
p.kill()
})
this[UNDERLINGS].clear()
}
actor.kill = function kill() {
if (this[DEAD]) return
this[DEAD] = true
this[TIMERS].slice().forEach(t => t())
delete this[TIMERS]
input.do_uncontrol(this)
this.clear()
this[OVERLING][UNDERLINGS].delete(this)
delete this[UNDERLINGS]
if (typeof this.garbage === "function") this.garbage()
if (typeof this.then === "function") this.then()
act.tag_clear_guid(this)
}
actor.kill.doc = `Remove this actor and all its underlings from existence.`
actor.delay = function(fn, seconds) {
if (this[DEAD]) return
add_timer(this, fn, seconds)
}
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`
actor[UNDERLINGS] = new Set()
ex[prosperon.DOC] = `
A set of utilities for iterating over a hierarchy of actor-like objects, as well
as managing tag-based lookups. Objects are assumed to have a "objects" property,
pointing to children or sub-objects, forming a tree.
`
function eachobj(obj, fn) {
var val = fn(obj)
if (val) return val
for (var o in obj.objects) {
if (obj.objects[o] === obj) console.error(`Object ${obj.toString()} is referenced by itself.`)
val = eachobj(obj.objects[o], fn)
if (val) return val
}
}
ex.all_objects = function (fn, startobj = world) {
return eachobj(startobj, fn)
}
ex.all_objects[prosperon.DOC] = `
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
:param startobj: The root object at which iteration begins, default is the global "world".
:return: The first truthy value returned by fn, or undefined if none.
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
`
ex.find_object = function (fn, startobj = world) {}
ex.find_object[prosperon.DOC] = `
:param fn: A callback or criteria to locate a particular object.
:param startobj: The root object at which search begins, default "world".
:return: Not yet implemented.
Intended to find a matching object within the hierarchy.
`
var gtags = {}
ex.tag_add = function (tag, obj) {
gtags[tag] ??= new Set()
gtags[tag].add(obj)
}
ex.tag_add[prosperon.DOC] = `
:param tag: A string tag to associate with the object.
:param obj: The object to add under this tag.
:return: None
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
`
ex.tag_rm = function (tag, obj) {
delete gtags[tag].delete(obj)
}
ex.tag_rm[prosperon.DOC] = `
:param tag: The tag to remove the object from.
:param obj: The object to remove from the tag set.
:return: None
Remove the given object from the specified tags set, if it exists.
`
ex.tag_clear_guid = function (obj) {
for (var tag in gtags) gtags[tag].delete(obj)
}
ex.tag_clear_guid[prosperon.DOC] = `
:param obj: The object whose tags should be cleared.
:return: None
Remove the object from all tag sets.
`
ex.objects_with_tag = function (tag) {
if (!gtags[tag]) return []
return Array.from(gtags[tag])
}
ex.objects_with_tag[prosperon.DOC] = `
:param tag: A string tag to look up.
:return: An array of objects associated with the given tag.
Retrieve all objects currently tagged with the specified tag.
`
function parse_file(content, file) {
if (!content) return {}
if (content.match()
if (!/^\s*---\s*$/m.test(content)) {
var part = content.trim()
if (part.match(/return\s+[^;]+;?\s*$/)) {
return { module: part }
}
return { program: part }
}
var parts = content.split(/\n\s*---\s*\n/)
var module = parts[0]
if (!/\breturn\b/.test(module))
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`)
try {
new Function(module)()
} catch (e) {
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message)
}
var pad = '\n'.repeat(module.split('\n').length + 4)
return {
module,
program: pad + parts[1]
}
}
// path is the path of a module or script to resolve
var script_fn = function script_fn(path, args) {
var parsed = {}
var file = resources.find_script(path)
if (!file) {
parsed.module_ret = bare_load(path)
if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
return parsed
}
var content = io.slurp(file)
var parsed = parse_file(content, file)
var module_name = file.name()
parsed.module_ret = bare_load(path)
parsed.module_ret ??= {}
if (parsed.module) {
// Create a context object with args
var context = Object.create(parsed.module_ret)
context.__args__ = args || []
var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports: exports}; var define = undefined; var arg = this.__args__; ${parsed.module}})`
var module_fn = js.eval(file, mod_script)
parsed.module_ret = module_fn.call(context)
if (parsed.module_ret === undefined || parsed.module_ret === null)
throw new Error(`Module ${module_name} must return a value`)
parsed.module_fn = module_fn
}
parsed.program ??= ""
var prog_script = `(function use_${module_name}($_) { var self = this; var $ = this.__proto__; ${parsed.program}})`
parsed.prog_fn = js.eval(file, prog_script)
return parsed
}.hashify()
return ex

792
scripts/modules/sdl_gpu.js Normal file
View File

@@ -0,0 +1,792 @@
var render = {}
var io = use('io')
var os = use('os')
var controller = use('controller')
var tracy = use('tracy')
var graphics = use('graphics')
var imgui = use('imgui')
var transform = use('transform')
var base_pipeline = {
vertex: "sprite.vert",
fragment: "sprite.frag",
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
fill: true, // false for lines
depth: {
compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
test: false,
write: false,
bias: 0,
bias_slope_scale: 0,
bias_clamp: 0
},
stencil: {
enabled: true,
front: {
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
depth_fail: "keep",
pass: "keep"
},
back: {
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
depth_fail: "keep",
pass: "keep"
},
test: true,
compare_mask: 0,
write_mask: 0
},
blend: {
enabled: false,
src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
dst_rgb: "zero",
op_rgb: "add", // add/sub/rev_sub/min/max
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
},
cull: "none", // none/front/back
face: "cw", // cw/ccw
alpha_to_coverage: false,
multisample: {
count: 1, // number of multisamples
mask: 0xFFFFFFFF,
domask: false
},
label: "scripted pipeline",
target: {}
}
var sprite_pipeline = Object.create(base_pipeline);
sprite_pipeline.blend = {
enabled:true,
src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
dst_rgb: "one_minus_src_alpha",
op_rgb: "add", // add/sub/rev_sub/min/max
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
};
var context;
sprite_pipeline.target = {
color_targets: [{
format:"rgba8",
blend:sprite_pipeline.blend
}],
depth: "d32 float s8"
};
var driver = "vulkan"
switch(os.platform()) {
case "Linux":
driver = "vulkan"
break
case "Windows":
// driver = "direct3d12"
driver = "vulkan"
break
case "macOS":
driver = "metal"
break
}
var unit_transform = new transform;
var cur = {};
cur.images = [];
cur.samplers = [];
var tbuffer;
function full_upload(buffers) {
var cmds = context.acquire_cmd_buffer();
tbuffer = context.upload(cmds, buffers, tbuffer);
cmds.submit();
}
full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit.
:param buffers: An array of data buffers to be uploaded.
:return: None
`
function bind_pipeline(pass, pipeline) {
make_pipeline(pipeline)
pass.bind_pipeline(pipeline.gpu)
pass.pipeline = pipeline;
}
bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass.
:param pass: The current render pass to bind the pipeline to.
:param pipeline: The pipeline object containing shader and state info.
:return: None
`
var main_pass;
var cornflower = [62/255,96/255,113/255,1];
function get_pipeline_ubo_slot(pipeline, name) {
if (!pipeline.vertex.reflection.ubos) return;
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
var ubo = pipeline.vertex.reflection.ubos[i];
if (ubo.name.endsWith(name))
return i;
}
return undefined;
}
get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix.
:param pipeline: The pipeline whose vertex reflection is inspected.
:param name: A string suffix to match against the uniform buffer block name.
:return: The integer index of the matching UBO, or undefined if not found.
`
function transpose4x4(val) {
var out = [];
out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12];
out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13];
out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
return out;
}
transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix.
:param val: An array of length 16 representing a 4x4 matrix in row-major format.
:return: A new array of length 16 representing the transposed matrix.
`
function ubo_obj_to_array(pipeline, name, obj) {
var ubo;
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
ubo = pipeline.vertex.reflection.ubos[i];
if (ubo.name.endsWith(name)) break;
}
var type = pipeline.vertex.reflection.types[ubo.type];
var len = 0;
for (var mem of type.members)
len += type_to_byte_count(mem.type);
var buf = new ArrayBuffer(len);
var view = new DataView(buf);
for (var mem of type.members) {
var val = obj[mem.name];
if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);
if (mem.name === 'model')
val = transpose4x4(val.array());
for (var i = 0; i < val.length; i++)
view.setFloat32(mem.offset + i*4, val[i],true);
}
return buf;
}
ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info.
:param pipeline: The pipeline whose vertex reflection is read for UBO structure.
:param name: The name suffix that identifies the target UBO in the reflection data.
:param obj: An object whose properties match the UBO members.
:return: An ArrayBuffer containing packed UBO data.
`
function type_to_byte_count(type) {
switch (type) {
case 'float': return 4;
case 'vec2': return 8;
case 'vec3': return 12;
case 'vec4': return 16;
case 'mat4': return 64;
default: throw new Error("Unknown or unsupported float-based type: " + type);
}
}
type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types.
:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4').
:return: Integer number of bytes.
`
var sprite_model_ubo = {
model: unit_transform,
color: [1,1,1,1]
};
var shader_cache = {};
var shader_times = {};
function make_pipeline(pipeline) {
if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made
if (typeof pipeline.vertex === 'string')
pipeline.vertex = make_shader(pipeline.vertex);
if (typeof pipeline.fragment === 'string')
pipeline.fragment = make_shader(pipeline.fragment)
// 1) Reflection data for vertex shader
var refl = pipeline.vertex.reflection
if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
pipeline.gpu = context.make_pipeline(pipeline);
return;
}
var inputs = refl.inputs
var buffer_descriptions = []
var attributes = []
// 2) Build buffer + attribute for each reflection input
for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i]
var typeStr = inp.type
var nameStr = (inp.name || "").toUpperCase()
var pitch = 4
var fmt = "float1"
if (typeStr == "vec2") {
pitch = 8
fmt = "float2"
} else if (typeStr == "vec3") {
pitch = 12
fmt = "float3"
} else if (typeStr == "vec4") {
if (nameStr.indexOf("COLOR") >= 0) {
pitch = 16
fmt = "color"
} else {
pitch = 16
fmt = "float4"
}
}
buffer_descriptions.push({
slot: i,
pitch: pitch,
input_rate: "vertex",
instance_step_rate: 0,
name:inp.name.split(".").pop()
})
attributes.push({
location: inp.location,
buffer_slot: i,
format: fmt,
offset: 0
})
}
pipeline.vertex_buffer_descriptions = buffer_descriptions
pipeline.vertex_attributes = attributes
pipeline.gpu = context.make_pipeline(pipeline);
}
make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created.
:param pipeline: An object describing the pipeline state, shaders, and reflection data.
:return: None
`
var shader_type;
function make_shader(sh_file) {
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
if (shader_cache[file]) return shader_cache[file]
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
var shader = {
code: io.slurpbytes(file),
format: shader_type,
stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
num_textures: 0,
num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
entrypoint: shader_type === "msl" ? "main0" : "main"
}
shader.gpu = context.make_shader(shader)
shader.reflection = refl;
shader_cache[file] = shader
shader.file = sh_file
return shader
}
make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded.
:param sh_file: The base filename (without extension) of the shader to compile.
:return: A shader object with GPU and reflection data attached.
`
var render_queue = [];
var hud_queue = [];
var current_queue = render_queue;
var std_sampler = {
min_filter: "nearest",
mag_filter: "nearest",
mipmap: "linear",
u: "repeat",
v: "repeat",
w: "repeat",
mip_bias: 0,
max_anisotropy: 0,
compare_op: "none",
min_lod: 0,
max_lod: 0,
anisotropy: false,
compare: false
};
function upload_model(model) {
var bufs = [];
for (var i in model) {
if (typeof model[i] !== 'object') continue;
bufs.push(model[i]);
}
context.upload(this, bufs);
}
upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU.
:param model: An object whose buffer properties are to be uploaded.
:return: None
`
function bind_model(pass, pipeline, model) {
var buffers = pipeline.vertex_buffer_descriptions;
var bufs = [];
if (buffers)
for (var b of buffers) {
if (b.name in model) bufs.push(model[b.name])
else throw Error (`could not find buffer ${b.name} on model`);
}
pass.bind_buffers(0,bufs);
pass.bind_index_buffer(model.indices);
}
bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass.
:param pass: The current render pass.
:param pipeline: The pipeline object with vertex buffer descriptions.
:param model: The model object containing matching buffers and an index buffer.
:return: None
`
function bind_mat(pass, pipeline, mat) {
var imgs = [];
var refl = pipeline.fragment.reflection;
if (refl.separate_images) {
for (var i of refl.separate_images) {
if (i.name in mat) {
var tex = mat[i.name];
imgs.push({texture:tex.texture, sampler:tex.sampler});
} else
throw Error (`could not find all necessary images: ${i.name}`)
}
pass.bind_samplers(false, 0,imgs);
}
}
bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader.
:param pass: The current render pass.
:param pipeline: The pipeline whose fragment shader reflection indicates required textures.
:param mat: An object mapping the required image names to {texture, sampler}.
:return: None
`
function group_sprites_by_texture(sprites, mesh) {
if (sprites.length === 0) return;
for (var i = 0; i < sprites.length; i++) {
sprites[i].mesh = mesh;
sprites[i].first_index = i*6;
sprites[i].num_indices = 6;
}
return;
// The code below is an alternate approach to grouping by image. Currently not in use.
/*
var groups = [];
var group = {image:sprites[0].image, first_index:0};
var count = 1;
for (var i = 1; i < sprites.length; i++) {
if (sprites[i].image === group.image) {
count++;
continue;
}
group.num_indices = count*6;
var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
group = newgroup;
groups.push(group);
count=1;
}
group.num_indices = count*6;
return groups;
*/
}
group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed.
:param sprites: An array of sprite objects.
:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite.
:return: None
`
var main_color = {
type:"2d",
format: "rgba8",
layers: 1,
mip_levels: 1,
samples: 0,
sampler:true,
color_target:true
};
var main_depth = {
type: "2d",
format: "d32 float s8",
layers:1,
mip_levels:1,
samples:0,
sampler:true,
depth_target:true
};
function render_camera(cmds, camera) {
var pass;
delete camera.target // TODO: HORRIBLE
if (!camera.target) {
main_color.width = main_depth.width = camera.size.x;
main_color.height = main_depth.height = camera.size.y;
camera.target = {
color_targets: [{
texture: context.texture(main_color),
mip_level:0,
layer: 0,
load:"clear",
store:"store",
clear: cornflower
}],
depth_stencil: {
texture: context.texture(main_depth),
clear:1,
load:"dont_care",
store:"dont_care",
stencil_load:"dont_care",
stencil_store:"dont_care",
stencil_clear:0
}
};
}
var buffers = [];
buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue));
var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
for (var q of unique_meshes)
buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]);
buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue));
for (var q of hud_queue)
if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]);
full_upload(buffers)
var pass = cmds.render_pass(camera.target);
var pipeline = sprite_pipeline;
bind_pipeline(pass,pipeline);
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.camera(camera, camslot);
modelslot = get_pipeline_ubo_slot(pipeline, "model");
if (typeof modelslot !== 'undefined') {
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
cmds.push_vertex_uniform_data(modelslot, ubo);
}
var mesh;
var img;
var modelslot;
cmds.push_debug_group("draw")
for (var group of render_queue) {
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group()
cmds.push_debug_group("hud")
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.hud(camera.size, camslot);
for (var group of hud_queue) {
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group();
pass?.end();
render_queue = [];
hud_queue = [];
}
render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items.
:param cmds: A command buffer obtained from the GPU context.
:param camera: The camera object (with size, optional target, etc.).
:return: None
`
var swaps = [];
render.present = function() {
os.clean_transforms();
var cmds = context.acquire_cmd_buffer();
render_camera(cmds, prosperon.camera);
var swapchain_tex = cmds.acquire_swapchain();
if (!swapchain_tex)
cmds.cancel();
else {
var torect = prosperon.camera.draw_rect(prosperon.window.size);
torect.texture = swapchain_tex;
if (swapchain_tex) {
cmds.blit({
src: prosperon.camera.target.color_targets[0].texture,
dst: torect,
filter:"nearest",
load: "clear"
});
if (imgui) { // draws any imgui commands present
cmds.push_debug_group("imgui")
imgui.prepend(cmds);
var pass = cmds.render_pass({
color_targets:[{texture:swapchain_tex}]});
imgui.endframe(cmds,pass);
pass.end();
cmds.pop_debug_group()
}
}
cmds.submit()
}
}
render.present[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available.
:return: None
`
var stencil_write = {
compare: "always",
fail_op: "replace",
depth_fail_op: "replace",
pass_op: "replace"
};
function stencil_writer(ref) {
var pipe = Object.create(base_pipeline);
Object.assign(pipe, {
stencil: {
enabled: true,
front: stencil_write,
back: stencil_write,
write:true,
read:true,
ref:ref
},
write_mask: colormask.none
});
return pipe;
}.hashify();
// objects by default draw where the stencil buffer is 0
function fillmask(ref) {
var pipe = stencil_writer(ref);
render.use_shader('screenfill.cg', pipe);
render.draw(shape.quad);
}
render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.
:param ref: The stencil reference value to write.
:return: None
`
var stencil_invert = {
compare: "always",
fail_op: "invert",
depth_fail_op: "invert",
pass_op: "invert"
};
function mask(image, pos, scale, rotation = 0, ref = 1) {
if (typeof image === 'string')
image = graphics.texture(image);
var tex = image.texture;
if (scale) scale = scale.div([tex.width,tex.height]);
else scale = [1,1,1]
var pipe = stencil_writer(ref);
render.use_shader('sprite.cg', pipe);
var t = new transform;
t.trs(pos, undefined, scale);
set_model(t);
render.use_mat({
diffuse:image.texture,
rect: image.rect,
shade: Color.white
});
render.draw(shape.quad);
}
render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value.
:param image: A texture or string path (which is converted to a texture).
:param pos: The translation (x, y) for the image placement.
:param scale: Optional scaling applied to the texture.
:param rotation: Optional rotation in radians (unused by default).
:param ref: The stencil reference value to write.
:return: None
`
render.viewport = function(rect) {
context.viewport(rect);
}
render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle.
:param rect: A rectangle [x, y, width, height].
:return: None
`
render.scissor = function(rect) {
render.viewport(rect)
}
render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport).
:param rect: A rectangle [x, y, width, height].
:return: None
`
var std_sampler
if (tracy) tracy.gpu_init()
render.queue = function(cmd) {
if (Array.isArray(cmd))
for (var i of cmd) current_queue.push(i)
else
current_queue.push(cmd)
}
render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called.
:param cmd: Either a single command object or an array of command objects.
:return: None
`
render.setup_draw = function() {
current_queue = render_queue;
prosperon.draw();
}
render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.
:return: None
`
render.setup_hud = function() {
current_queue = hud_queue;
prosperon.hud();
}
render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.
:return: None
`
render.initialize = function(config)
{
var default_conf = {
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
width: 1280,
height: 720,
icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
high_dpi:0,
alpha:1,
fullscreen:0,
sample_count:1,
enable_clipboard:true,
enable_dragndrop: true,
max_dropped_files: 1,
swap_interval: 1,
name: "Prosperon",
version:prosperon.version + "-" + prosperon.revision,
identifier: "world.pockle.prosperon",
creator: "Pockle World LLC",
copyright: "Copyright Pockle World 2025",
type: "game",
url: "https://prosperon.dev"
}
config.__proto__ = default_conf
prosperon.camera = use('ext/camera').make()
prosperon.camera.size = [config.width,config.height]
prosperon.window = prosperon.engine_start(config)
context = prosperon.window.make_gpu(false,driver)
context.window = prosperon.window
context.claim_window(prosperon.window)
context.set_swapchain('sdr', 'vsync')
if (imgui) imgui.init(context, prosperon.window)
shader_type = context.shader_format()[0];
std_sampler = context.make_sampler({
min_filter: "nearest",
mag_filter: "nearest",
mipmap_mode: "nearest",
address_mode_u: "repeat",
address_mode_v: "repeat",
address_mode_w: "repeat"
});
}
return render

View File

@@ -0,0 +1,125 @@
var render = {}
var context
var util = use('util')
render.initialize = function(config)
{
var default_conf = {
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
width: 1280,
height: 720,
// icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
high_dpi:0,
alpha:1,
fullscreen:0,
sample_count:1,
enable_clipboard:true,
enable_dragndrop: true,
max_dropped_files: 1,
swap_interval: 1,
name: "Prosperon",
version:prosperon.version + "-" + prosperon.revision,
identifier: "world.pockle.prosperon",
creator: "Pockle World LLC",
copyright: "Copyright Pockle World 2025",
type: "game",
url: "https://prosperon.dev"
}
config.__proto__ = default_conf
prosperon.window = prosperon.engine_start(config)
context = prosperon.window.make_renderer()
context.logical_size([config.resolution_x, config.resolution_y], config.mode)
}
render.sprite = function(sprite)
{
context.sprite(sprite)
}
// img here is the engine surface
render.load_texture = function(surface)
{
return context.load_texture(surface)
}
var current_color = Color.white
render.image = function(image, rect, rotation, anchor, shear, info)
{
// rect.width = image.rect_px.width;
// rect.height = image.rect_px.height;
image.texture.mode(info.mode)
context.texture(image.texture, image.rect_px, rect, rotation, anchor);
}
render.clip = function(rect)
{
context.clip(rect)
}
render.line = function(points)
{
context.line(points)
}
render.point = function(pos)
{
context.point(pos)
}
render.rectangle = function(rect)
{
context.rects([rect])
}
render.rects = function(rects)
{
context.rects(rects)
}
render.pipeline = function(pipe)
{
// any changes here
}
render.settings = function(set)
{
if (!set.color) return
context.draw_color(set.color)
}
render.geometry = function(image, mesh, pipeline)
{
context.geometry(image, mesh)
}
render.slice9 = function(image, rect, slice, info, pipeline)
{
context.slice9(image.texture, image.rect_px, util.normalizeSpacing(slice), rect);
}
render.get_image = function(rect)
{
return context.get_image(rect)
}
render.clear = function(color)
{
if (color) context.draw_color(color)
context.clear()
}
render.present = function()
{
context.present()
}
render.camera = function(cam)
{
context.camera(cam);
}
return render

View File

@@ -16,6 +16,7 @@ audio.pcm = function pcm(file)
file = res.find_sound(file); file = res.find_sound(file);
if (!file) throw new Error(`Could not findfile ${file}`); if (!file) throw new Error(`Could not findfile ${file}`);
if (pcms[file]) return pcms[file]; if (pcms[file]) return pcms[file];
var bytes = io.slurpbytes(file)
var newpcm = soloud.load_wav_mem(io.slurpbytes(file)); var newpcm = soloud.load_wav_mem(io.slurpbytes(file));
pcms[file] = newpcm; pcms[file] = newpcm;
return newpcm; return newpcm;
@@ -73,4 +74,33 @@ audio.music = function music(file, fade = 0.5) {
}; };
audio.music[doc.sym] = `Play the given music file, with an optional cross fade. The song will loop. When this is invoked again, the previous music is replaced.` audio.music[doc.sym] = `Play the given music file, with an optional cross fade. The song will loop. When this is invoked again, the previous music is replaced.`
var ss = use('sdl_audio')
var feeder = ss.open_stream("playback")
feeder.set_format({format:"f32", channels:2,samplerate:44100})
feeder.resume()
var FRAMES = 1024
var CHANNELS = 2
var BYTES_PER_F = 4
var SAMPLES = FRAMES * CHANNELS
var CHUNK_BYTES = FRAMES * CHANNELS * BYTES_PER_F
var mixview = new Float32Array(FRAMES*CHANNELS)
var mixbuf = mixview.buffer
function pump()
{
if (feeder.queued() < CHUNK_BYTES*3) {
var mm = soloud.mix(FRAMES)
feeder.put(mm)
}
$_.delay(pump, 1/240)
}
pump()
return audio; return audio;

View File

@@ -42,16 +42,16 @@ time.strparse = {
s: "second", s: "second",
}; };
time.isleap = function(year) { time.isleap = function isleap(year) {
return this.yearsize(year) === 366; return this.yearsize(year) === 366;
}; };
time.yearsize = function(y) { time.yearsize = function yearsize(y) {
if (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) return 366; if (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) return 366;
return 365; return 365;
}; };
time.timecode = function(t, fps = 24) { time.timecode = function timecode(t, fps = 24) {
var s = Math.trunc(t); var s = Math.trunc(t);
t -= s; t -= s;
return `${s}:${Math.trunc(fps * s)}`; return `${s}:${Math.trunc(fps * s)}`;
@@ -60,7 +60,7 @@ time.timecode = function(t, fps = 24) {
time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
time.zones = { "-12": "IDLW" }; time.zones = { "-12": "IDLW" };
time.record = function(num, zone = this.computer_zone()) { time.record = function record(num, zone = this.computer_zone()) {
if (typeof num === "object") { if (typeof num === "object") {
return num; return num;
} else if (typeof num === "number") { } else if (typeof num === "number") {
@@ -113,7 +113,7 @@ time.record = function(num, zone = this.computer_zone()) {
} }
}; };
time.number = function(rec) { time.number = function number(rec) {
if (typeof rec === "number") { if (typeof rec === "number") {
return rec; return rec;
} else if (typeof rec === "object") { } else if (typeof rec === "object") {
@@ -153,7 +153,7 @@ time.number = function(rec) {
time.fmt = "vB mB d h:nn:ss TZz a y c"; time.fmt = "vB mB d h:nn:ss TZz a y c";
time.text = function(num, fmt = this.fmt, zone) { time.text = function text(num, fmt = this.fmt, zone) {
var rec = (typeof num === "number") ? time.record(num, zone) : num; var rec = (typeof num === "number") ? time.record(num, zone) : num;
zone = rec.zone; zone = rec.zone;
if (fmt.match("a")) { if (fmt.match("a")) {
@@ -187,7 +187,7 @@ time.text = function(num, fmt = this.fmt, zone) {
fmt = fmt.replaceAll("h", rec.hour); fmt = fmt.replaceAll("h", rec.hour);
fmt = fmt.replaceAll("nn", rec.minute.toString().padStart(2, "0")); fmt = fmt.replaceAll("nn", rec.minute.toString().padStart(2, "0"));
fmt = fmt.replaceAll("n", rec.minute); fmt = fmt.replaceAll("n", rec.minute);
fmt = fmt.replaceAll("ss", rec.second.toString().padStart(2, "0")); fmt = fmt.replaceAll("ss", rec.second.toFixed(2).padStart(2, "0"));
fmt = fmt.replaceAll("s", rec.second); fmt = fmt.replaceAll("s", rec.second);
fmt = fmt.replaceAll("z", zone >= 0 ? "+" + zone : zone); fmt = fmt.replaceAll("z", zone >= 0 ? "+" + zone : zone);
fmt = fmt.replaceAll(/mm[^bB]/g, rec.month + 1); fmt = fmt.replaceAll(/mm[^bB]/g, rec.month + 1);

43
scripts/modules/wota.js Normal file
View File

@@ -0,0 +1,43 @@
// wota
var wota = this
var json = use('json')
var encode = wota.encode
function wota_tostring()
{
return json.encode(wota.decode(this))
}
var wota_obj = {
toString: wota_tostring
}
wota.encode = function(obj, replacer)
{
var result = encode(obj, replacer)
result.toString = wota_tostring
return result
}
wota.encode[prosperon.DOC] = `Convert a JavaScript value into a WOTA-encoded ArrayBuffer.
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the WOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
:param value: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
:param replacer: An optional function that alters the encoding behavior for specific values.
:return: An ArrayBuffer containing the WOTA-encoded data.
:throws: An error if no argument is provided or if a cyclic object is encountered.
`
wota.decode[prosperon.DOC] = `Decode a WOTA-encoded ArrayBuffer into a JavaScript value.
This function deserializes a WOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
:param buffer: An ArrayBuffer containing WOTA-encoded data to decode.
:param reviver: An optional function that transforms the decoded values.
:return: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
`
return wota

View File

@@ -1,5 +1,7 @@
#include "HandmadeMath.h" #include "HandmadeMath.h"
#include <stdlib.h>
const HMM_Vec2 v2zero = {0,0}; const HMM_Vec2 v2zero = {0,0};
const HMM_Vec2 v2one = {1,1}; const HMM_Vec2 v2one = {1,1};
const HMM_Vec3 v3zero = {0,0,0}; const HMM_Vec3 v3zero = {0,0,0};

View File

@@ -96,8 +96,6 @@
#ifndef HANDMADE_MATH_H #ifndef HANDMADE_MATH_H
#define HANDMADE_MATH_H #define HANDMADE_MATH_H
#include <chipmunk/chipmunk.h>
#if !defined(HANDMADE_MATH_NO_SIMD) #if !defined(HANDMADE_MATH_NO_SIMD)
#if defined(__ARM_NEON) || defined(__ARM_NEON__) #if defined(__ARM_NEON) || defined(__ARM_NEON__)
#define HANDMADE_MATH__USE_NEON 1 #define HANDMADE_MATH__USE_NEON 1
@@ -272,12 +270,6 @@ typedef union HMM_Vec3 {
HMM_Vec2 VW; HMM_Vec2 VW;
}; };
struct
{
HMM_Vec2 cp;
float _Ignored5;
};
float Elements[3]; float Elements[3];
float e[3]; float e[3];
@@ -373,12 +365,6 @@ typedef union HMM_Vec4 {
HMM_Vec2 ZW; HMM_Vec2 ZW;
}; };
struct
{
HMM_Vec2 cp;
HMM_Vec2 wh;
};
HMM_Quat quat; HMM_Quat quat;
struct {float x, y, z, w; }; struct {float x, y, z, w; };
struct {float r, g, b, a; }; struct {float r, g, b, a; };

View File

@@ -1,34 +0,0 @@
#include <stdlib.h>
#include <math.h>
#include "aabb.h"
aabb*
aabb_new(float x, float y, float hW, float hH) {
aabb* a = malloc(sizeof(aabb));
a->center.x = x;
a->center.y = y;
a->dims.w = hW;
a->dims.h = hH;
return a;
}
void
aabb_free(aabb *a) {
free(a);
}
int
aabb_contains(aabb *a, float x, float y) {
return (x >= a->center.x-a->dims.w &&
x <= a->center.x+a->dims.w) &&
(y >= a->center.y-a->dims.h &&
y <= a->center.y+a->dims.h);
}
int
aabb_intersects(aabb *a, aabb *b) {
return (abs(a->center.x - b->center.x) < (a->dims.w + b->dims.w)) &&
(abs(a->center.y - b->center.y) < (a->dims.h + b->dims.h));
}

View File

@@ -1,48 +0,0 @@
/*
aabb.h
2014 JSK (kutani@projectkutani.com)
Simple (2D) axis-aligned bounding box implementation. Part of the Panic
Panic project.
Released to the public domain. See LICENSE for details.
*/
#ifndef _AABB_H
#define _AABB_H
/** \brief axis-aligned bounding box
Simple struct of four floats, divided into two sub-structs.
center {x, y} - The center point of the bounding box
dims {w, h} - The half-width and half-height of the box
*/
typedef struct aabb {
struct {
float x;
float y;
} center;
struct {
float w;
float h;
} dims;
} aabb;
/// Malloc's a new aabb struct
/*!
Mallocs a new aabb struct and sets center and dims to the passed
x, y, hW, and hH values.
*/
aabb* aabb_new(float x, float y, float hW, float hH);
/// Frees the passed aabb.
void aabb_free(aabb *a);
/// Checks if the point x,y lies within the passed aabb
int aabb_contains(aabb *a, float x, float y);
/// Checks if the two passed aabb's intersect
int aabb_intersects(aabb *a, aabb *b);
#endif

View File

@@ -16,7 +16,10 @@ void animation_run(struct animation *anim, float now)
HMM_Vec4 sample_cubicspline(sampler *sampler, float t, int prev, int next) HMM_Vec4 sample_cubicspline(sampler *sampler, float t, int prev, int next)
{ {
return (HMM_Vec4)HMM_SLerp(HMM_QV4(sampler->data[prev]), t, HMM_QV4(sampler->data[next])); HMM_Vec4 ret;
HMM_Quat qv = HMM_SLerp(HMM_QV4(sampler->data[prev]), t, HMM_QV4(sampler->data[next]));
memcpy(ret.e, qv.e, sizeof(ret.e));
return ret;
} }
HMM_Vec4 sample_sampler(sampler *sampler, float time) HMM_Vec4 sample_sampler(sampler *sampler, float time)
@@ -37,6 +40,9 @@ HMM_Vec4 sample_sampler(sampler *sampler, float time)
float td = sampler->times[next_time]-sampler->times[previous_time]; float td = sampler->times[next_time]-sampler->times[previous_time];
float t = (time - sampler->times[previous_time])/td; float t = (time - sampler->times[previous_time])/td;
HMM_Vec4 ret;
HMM_Quat qv;
switch(sampler->type) { switch(sampler->type) {
case LINEAR: case LINEAR:
return HMM_LerpV4(sampler->data[previous_time],time,sampler->data[next_time]); return HMM_LerpV4(sampler->data[previous_time],time,sampler->data[next_time]);
@@ -48,7 +54,9 @@ HMM_Vec4 sample_sampler(sampler *sampler, float time)
return sample_cubicspline(sampler,t, previous_time, next_time); return sample_cubicspline(sampler,t, previous_time, next_time);
break; break;
case SLERP: case SLERP:
return (HMM_Vec4)HMM_SLerp(sampler->data[previous_time].quat, time, sampler->data[next_time].quat); qv = HMM_SLerp(sampler->data[previous_time].quat, time, sampler->data[next_time].quat);
memcpy(ret.e,qv.e,sizeof(ret.e));
return ret;
break; break;
} }
return sample_cubicspline(sampler,t, previous_time, next_time); return sample_cubicspline(sampler,t, previous_time, next_time);

View File

@@ -24,11 +24,12 @@
#define STBI_NO_STDIO #define STBI_NO_STDIO
#include "stb_image.h" #include "stb_image.h"
#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_BOX
#define STB_IMAGE_RESIZE_IMPLEMENTATION #define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize2.h" #include "stb_image_resize2.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_BOX
#include "stb_image_write.h" #include "stb_image_write.h"
#define PL_MPEG_IMPLEMENTATION #define PL_MPEG_IMPLEMENTATION

View File

@@ -306,24 +306,7 @@ struct ase_t
void* mem_ctx; void* mem_ctx;
}; };
#define ASEPRITE_ERROR_MAX 256 const char *aseprite_GetError(void);
static char aseprite_error[ASEPRITE_ERROR_MAX] = {0};
static const char *aseprite_GetError() {
return aseprite_error;
}
static void aseprite_clear_error() {
aseprite_error[0] = 0;
}
static void aseprite_set_error(const char *msg) {
if (msg) {
strncpy(aseprite_error, msg, ASEPRITE_ERROR_MAX-1);
aseprite_error[ASEPRITE_ERROR_MAX-1] = 0;
} else
aseprite_error[0] = 0;
}
#endif // CUTE_ASEPRITE_H #endif // CUTE_ASEPRITE_H
@@ -331,6 +314,24 @@ static void aseprite_set_error(const char *msg) {
#ifndef CUTE_ASEPRITE_IMPLEMENTATION_ONCE #ifndef CUTE_ASEPRITE_IMPLEMENTATION_ONCE
#define CUTE_ASEPRITE_IMPLEMENTATION_ONCE #define CUTE_ASEPRITE_IMPLEMENTATION_ONCE
#define ASEPRITE_ERROR_MAX 256
char aseprite_error[ASEPRITE_ERROR_MAX] = {0};
const char *aseprite_GetError(void) {
return aseprite_error;
}
void aseprite_clear_error(void) {
aseprite_error[0] = 0;
}
void aseprite_set_error(const char *msg) {
if (msg) {
strncpy(aseprite_error, msg, ASEPRITE_ERROR_MAX-1);
aseprite_error[ASEPRITE_ERROR_MAX-1] = 0;
} else
aseprite_error[0] = 0;
}
#ifndef _CRT_SECURE_NO_WARNINGS #ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS
@@ -725,7 +726,6 @@ static int s_inflate(const void* in, int in_bytes, void* out, int out_bytes, voi
s->out_end = s->out + out_bytes; s->out_end = s->out + out_bytes;
s->begin = (char*)out; s->begin = (char*)out;
int count = 0;
uint32_t bfinal; uint32_t bfinal;
do do
{ {
@@ -739,8 +739,6 @@ static int s_inflate(const void* in, int in_bytes, void* out, int out_bytes, voi
case 2: s_dynamic(s); CUTE_ASEPRITE_CALL(s_block(s)); break; case 2: s_dynamic(s); CUTE_ASEPRITE_CALL(s_block(s)); break;
case 3: CUTE_ASEPRITE_CHECK(0, "Detected unknown block type within input stream."); case 3: CUTE_ASEPRITE_CHECK(0, "Detected unknown block type within input stream.");
} }
++count;
} }
while (!bfinal); while (!bfinal);

View File

@@ -15,11 +15,6 @@ void datastream_free(JSRuntime *rt,datastream *ds)
free(ds); free(ds);
} }
static void render_audio(plm_t *mpeg, plm_samples_t *samples, struct datastream *ds) {
// for (int i = 0; i < samples->count * CHANNELS; i++)
// ringpush(ds->ring, samples->interleaved[i]);
}
struct datastream *ds_openvideo(void *raw, size_t rawlen) struct datastream *ds_openvideo(void *raw, size_t rawlen)
{ {
struct datastream *ds = malloc(sizeof(*ds)); struct datastream *ds = malloc(sizeof(*ds));

1744
source/dmon.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,31 +21,6 @@ void font_free(JSRuntime *rt, font *f)
free(f); free(f);
} }
struct sFont *MakeSDFFont(const char *fontfile, int height)
{
int packsize = 1024;
struct sFont *newfont = calloc(1, sizeof(struct sFont));
newfont->height = height;
char fontpath[256];
snprintf(fontpath, 256, "fonts/%s", fontfile);
// unsigned char *ttf_buffer = slurp_file(fontpath, NULL);
unsigned char *bitmap = malloc(packsize * packsize);
stbtt_fontinfo fontinfo;
// if (!stbtt_InitFont(&fontinfo, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer, 0))) {
// YughError("Failed to make font %s", fontfile);
// }
for (int i = 32; i < 95; i++) {
int w, h, xoff, yoff;
// unsigned char *stbtt_GetGlyphSDF(&fontinfo, height, i, 1, 0, 1, &w, &h, &xoff, &yoff);
}
return newfont;
}
struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) { struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) {
if (!ttf_buffer) if (!ttf_buffer)
return NULL; return NULL;
@@ -111,29 +86,6 @@ struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) {
return newfont; return newfont;
} }
int text_flush() {
/* if (arrlen(text_buffer) == 0) return 0;
sg_range verts;
verts.ptr = text_buffer;
verts.size = sizeof(struct text_vert) * arrlen(text_buffer);
if (sg_query_buffer_will_overflow(*buf, verts.size)) {
sg_destroy_buffer(*buf);
*buf = sg_make_buffer(&(sg_buffer_desc){
.size = verts.size,
.type = SG_BUFFERTYPE_STORAGEBUFFER,
.usage = SG_USAGE_STREAM,
.label = "text buffer"
});
}
sg_append_buffer(*buf, &verts);
int n = arrlen(text_buffer);
arrsetlen(text_buffer, 0);
return n;
*/
}
void sdrawCharacter(struct text_vert **buffer, stbtt_packedchar c, HMM_Vec2 cursor, float scale, struct rgba color) { void sdrawCharacter(struct text_vert **buffer, stbtt_packedchar c, HMM_Vec2 cursor, float scale, struct rgba color) {
struct text_vert vert; struct text_vert vert;
@@ -185,7 +137,7 @@ const char *esc_color(const char *c, struct rgba *color, struct rgba defc)
{ {
struct rgba d; struct rgba d;
if (!color) color = &d; if (!color) color = &d;
if (*c != '\e') return c; if (*c != '\033') return c;
c++; c++;
if (*c != '[') return c; if (*c != '[') return c;
c++; c++;
@@ -235,7 +187,7 @@ HMM_Vec2 measure_text(const char *text, font *f, float size, float letterSpacing
continue; continue;
} }
float charWidth = f->Characters[*c].advance + letterSpacing; float charWidth = f->Characters[(unsigned char)*c].advance + letterSpacing;
// Handle wrapping // Handle wrapping
if (wrap > 0 && lineWidth + charWidth > wrap) { if (wrap > 0 && lineWidth + charWidth > wrap) {
@@ -283,15 +235,13 @@ HMM_Vec2 measure_text(const char *text, font *f, float size, float letterSpacing
} }
/* pos given in screen coordinates */ /* pos given in screen coordinates */
struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, float scale, colorf color, float wrap) { struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, float scale, colorf color, float wrap) {
int wrapAtWord = 1;
text_vert *buffer = NULL; text_vert *buffer = NULL;
int len = strlen(text);
HMM_Vec2 cursor = pos; HMM_Vec2 cursor = pos;
float lineHeight = f->ascent - f->descent; float lineHeight = f->ascent - f->descent;
float lineWidth = 0; float lineWidth = 0;
for (char *c = text; *c != 0; c++) { for (const char *c = text; *c != 0; c++) {
if (*c == '\n') { if (*c == '\n') {
cursor.x = pos.x; cursor.x = pos.x;
cursor.y -= lineHeight + f->linegap; cursor.y -= lineHeight + f->linegap;
@@ -299,7 +249,7 @@ struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, float scal
continue; continue;
} }
struct character chara = f->Characters[*c]; struct character chara = f->Characters[(unsigned char)*c];
if (wrap > 0 && lineWidth + chara.advance > wrap) { if (wrap > 0 && lineWidth + chara.advance > wrap) {
cursor.x = pos.x; cursor.x = pos.x;

View File

@@ -60,7 +60,4 @@ struct sFont *MakeFont(void *data, size_t len, int height);
struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, float scale, colorf color, float wrap); struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, float scale, colorf color, float wrap);
HMM_Vec2 measure_text(const char *text, font *f, float scale, float letterSpacing, float wrap); HMM_Vec2 measure_text(const char *text, font *f, float scale, float letterSpacing, float wrap);
// Flushes all letters from renderText calls into the provided buffer
int text_flush();
#endif #endif

Some files were not shown because too many files have changed in this diff Show More