81 Commits

Author SHA1 Message Date
John Alanbrook
216ada5568 attempt fix local network
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-linux (push) Failing after 1m33s
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 15:40:22 -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
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
045c4b49ef Add versioning dropdown to docs 2025-02-25 10:14:17 -06:00
143 changed files with 25258 additions and 5361 deletions

View File

@@ -7,6 +7,9 @@ on:
pull_request: pull_request:
jobs: jobs:
# ──────────────────────────────────────────────────────────────
# LINUX BUILD
# ──────────────────────────────────────────────────────────────
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
@@ -15,20 +18,17 @@ jobs:
steps: steps:
- name: Check Out Code - name: Check Out Code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with: { fetch-depth: 0 }
fetch-depth: 0
- name: Build Prosperon (Linux) - name: Build Prosperon (Linux)
run: | run: |
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -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
strip build/prosperon
- name: Upload Test Log (Linux) - name: Upload Test Log (Linux)
if: ${{ always() }} if: ${{ always() }}
@@ -44,11 +44,13 @@ jobs:
name: prosperon-artifacts-linux name: prosperon-artifacts-linux
path: build/prosperon path: build/prosperon
# ──────────────────────────────────────────────────────────────
# WINDOWS BUILD (MSYS2 / CLANG64)
# ──────────────────────────────────────────────────────────────
build-windows: build-windows:
runs-on: win-native runs-on: win-native
strategy: strategy:
matrix: matrix: { msystem: [ CLANG64 ] }
msystem: [CLANG64]
steps: steps:
- name: Check Out Code - name: Check Out Code
@@ -61,32 +63,26 @@ jobs:
update: true update: true
cache: true cache: true
install: | install: |
git git zip gzip tar base-devel
zip
gzip
tar
base-devel
pacboy: | pacboy: |
meson meson
cmake cmake
toolchain toolchain
- name: Build Prosperon - name: Build Prosperon (Windows)
shell: msys2 {0} shell: msys2 {0}
run: | run: |
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
meson compile -C build meson compile -C build
- name: Test Prosperon - name: Test Prosperon (Windows)
shell: msys2 {0} shell: msys2 {0}
continue-on-error: true
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
strip build/prosperon.exe
- 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:
@@ -100,15 +96,52 @@ jobs:
name: prosperon-artifacts-windows name: prosperon-artifacts-windows
path: build/prosperon.exe 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:
needs: [build-linux, build-windows] needs: [ build-linux, build-windows, build-macos ]
if: startsWith(github.ref, 'refs/tags/v') 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: with: { fetch-depth: 0 }
fetch-depth: 0
- name: Get Latest Tag - name: Get Latest Tag
id: get_tag id: get_tag
@@ -128,16 +161,21 @@ 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/
mkdir dist/linux
mkdir dist/win
cp linux_artifacts/* dist/linux/ cp linux_artifacts/* dist/linux/
cp windows_artifacts/* dist/win/ cp windows_artifacts/* dist/win/
cp mac_artifacts/* dist/mac/
- name: Package Final Dist - name: Package Final Dist
run: | run: |
@@ -151,14 +189,17 @@ jobs:
name: "prosperon-${{ steps.get_tag.outputs.tag }}" name: "prosperon-${{ steps.get_tag.outputs.tag }}"
path: "prosperon-${{ steps.get_tag.outputs.tag }}.zip" path: "prosperon-${{ steps.get_tag.outputs.tag }}.zip"
# ──────────────────────────────────────────────────────────────
# DEPLOY TO ITCH.IO (single ZIP containing all OSes)
# ──────────────────────────────────────────────────────────────
deploy-itch: deploy-itch:
needs: [package-dist] needs: [ package-dist ]
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: with: { fetch-depth: 0 }
fetch-depth: 0
- name: Get Latest Tag - name: Get Latest Tag
id: get_tag id: get_tag
@@ -177,24 +218,29 @@ jobs:
- name: Push to itch.io - name: Push to itch.io
run: | run: |
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" ${{ secrets.ITCHIO_USERNAME }}/prosperon:win-linux --userversion ${{ steps.get_tag.outputs.tag }} butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" \
${{ secrets.ITCHIO_USERNAME }}/prosperon:universal \
--userversion ${{ steps.get_tag.outputs.tag }}
env: env:
BUTLER_API_KEY: ${{ secrets.ITCHIO_API_KEY }} BUTLER_API_KEY: ${{ secrets.ITCHIO_API_KEY }}
# ──────────────────────────────────────────────────────────────
# DEPLOY TO SELF-HOSTED GITEA
# ──────────────────────────────────────────────────────────────
deploy-gitea: deploy-gitea:
needs: [package-dist] needs: [ package-dist ]
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: with: { fetch-depth: 0 }
fetch-depth: 0
- name: Get Latest Tag and Commit Message - name: Get Latest Tag & Commit Message
id: get_tag id: get_tag
run: | run: |
TAG=$(git describe --tags --abbrev=0) TAG=$(git describe --tags --abbrev=0)
COMMIT_MSG=$(git log -1 --pretty=%B "${TAG}") COMMIT_MSG=$(git log -1 --pretty=%B "$TAG")
echo "tag=$TAG" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
@@ -204,39 +250,26 @@ jobs:
name: "prosperon-${{ steps.get_tag.outputs.tag }}" name: "prosperon-${{ steps.get_tag.outputs.tag }}"
path: dist path: dist
- name: Create or Update Gitea Release - name: Create / Update Gitea Release
run: | run: |
TAG=${{ steps.get_tag.outputs.tag }} TAG=${{ steps.get_tag.outputs.tag }}
ZIP_FILE="dist/prosperon-${TAG}.zip" ZIP=dist/prosperon-${TAG}.zip
COMMIT_MSG=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.') 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')
# Check if release exists if [ "$RELEASE" = "null" ] || [ -z "$RELEASE" ]; then
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \ RELEASE=$(curl -X POST \
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/${TAG}" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
# Create a new release if it doesn't exist
echo "Creating new release for tag ${TAG}"
RELEASE_ID=$(curl -X POST \
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \ -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"${TAG}\",\"body\":${COMMIT_MSG},\"draft\":false,\"prerelease\":false}" \ -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') "https://gitea.pockle.world/api/v1/repos/john/prosperon/releases" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
echo "Failed to create release for tag ${TAG}"
exit 1
fi
echo "Created release with ID: ${RELEASE_ID}"
else
echo "Release already exists with ID: ${RELEASE_ID}"
fi fi
# Upload the zip file as an asset
curl -X POST \ curl -X POST \
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \ -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary @"${ZIP_FILE}" \ --data-binary @"$ZIP" \
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/${RELEASE_ID}/assets?name=prosperon-${TAG}.zip" "https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/$RELEASE/assets?name=prosperon-${TAG}.zip"
env: env:
TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }} 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

371
CLAUDE.md Normal file
View File

@@ -0,0 +1,371 @@
# 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)`
- Manage actor hierarchy with overlings and underlings
- Schedule actor tasks with `delay()` method
- Clean up with `kill()` and `garbage()`
### 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 Header Management
Messages contain `__HEADER__` information that can cause issues:
```javascript
// CORRECT: Extract clean actor reference
$_.receiver(e => {
if (e.type === 'join_game') {
var opponent = e.__HEADER__.replycc; // Clean actor reference
$_.send(opponent, {type: 'game_start'});
}
});
// WRONG: Using message object directly
$_.receiver(e => {
opponent = e; // Contains return headers that pollute future sends
});
```
### 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

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

@@ -2,6 +2,6 @@ Thank you for using Prosperon!
Provided are prosperon builds for all available platforms. Simply run prosperon for your platform in a game folder to play! 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, 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.
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

@@ -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'

3
examples/chess/Makefile Normal file
View File

@@ -0,0 +1,3 @@
def:
cd _prosperon && make && cp build_dbg/prosperon ..
./prosperon

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

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

@@ -0,0 +1,422 @@
/* main.js runs the demo with your prototype-based grid */
var moth = use('moth')
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]});
}
}
}
prosperon.on('draw', function() {
drawBoard()
drawPieces()
})
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();
console.log("Starting server with actor:", json.encode($_));
$_.portal(e => {
console.log("Portal received contact message:", json.encode(e));
// The proper Misty pattern: Portal should only reply with an actor reference
// Use a clean actor object, not application data
$_.send(e, { id: $_.id }); // Send a clean actor reference
console.log("Portal replied with server actor reference");
}, 5678);
}
function joinServer() {
gameState = 'searching';
updateTitle();
console.log("Client attempting to join server with client actor:", json.encode($_));
function contact_fn(actor, reason) {
console.log("Contact callback received:", actor ? "SUCCESS" : "FAILED", reason);
if (actor) {
// Ensure we have a clean actor reference with just the id
if (typeof actor === 'object' && actor.id) {
// Store server actor reference for ongoing communication
opponent = { id: actor.id }; // Clean actor reference
console.log("Connection established with server actor:", json.encode(opponent));
// Now that we have the server actor reference, send application message
// This follows the two-phase Misty pattern:
// 1. First establish actor connection (done with contact)
// 2. Then send application messages
console.log("Sending greet message to server");
$_.send(opponent, {
type: 'greet',
client_actor: { id: $_.id } // Send clean actor reference
});
// Update game state now that we're connected
gameState = 'connected';
updateTitle();
} else {
console.log("Received invalid actor reference:", json.encode(actor));
gameState = 'waiting';
updateTitle();
}
} else {
console.log(`Failed to connect: ${json.encode(reason)}`);
gameState = 'waiting';
updateTitle();
}
}
// Initial contact phase - get actor reference only
$_.contact(contact_fn, {
address: "localhost",
port: 5678
});
}
var os = use('os')
// Set up IO actor subscription
var ioguy = { id: os.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
if (e.client_actor && e.client_actor.id) {
opponent = { id: e.client_actor.id }; // Clean actor reference
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 {
console.log("Invalid client actor in greet message:", json.encode(e));
}
}
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

@@ -32,8 +32,9 @@ 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 = []
@@ -69,7 +70,10 @@ sdl3_opts.add_cmake_defines({
'SDL_STATIC': 'ON', 'SDL_STATIC': 'ON',
'SDL_SHARED': 'OFF', 'SDL_SHARED': 'OFF',
'SDL_TEST': 'OFF', 'SDL_TEST': 'OFF',
'CMAKE_BUILD_TYPE': 'Release' 'CMAKE_BUILD_TYPE': 'Release',
'SDL_THREADS': 'ON',
'SDL_PIPEWIRE': 'ON',
'SDL_PULSEAUDIO': 'ON',
}) })
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
@@ -94,58 +98,147 @@ if host_machine.system() == 'windows'
deps += cc.find_library('imm32') deps += cc.find_library('imm32')
deps += cc.find_library('version') deps += cc.find_library('version')
deps += cc.find_library('cfgmgr32') deps += cc.find_library('cfgmgr32')
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # TODO: A hack to get this to compile on MSYS2; otherwise it doesn't link correctly 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' 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
sdl3_proj = cmake.subproject('sdl3', options: sdl3_opts)
deps += sdl3_proj.dependency('SDL3-static')
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-miniz',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', static:true) deps += dependency('chipmunk', static:true)
deps += dependency('enet', static:true)
if host_machine.system() != 'emscripten'
deps += dependency('enet', static:true)
src += 'qjs_enet.c'
src += 'qjs_tracy.c'
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
deps += dependency('soloud', static:true) deps += dependency('soloud', static:true)
deps += dependency('libqrencode', static:true)
#deps += dependency('qjs-chipmunk', static:false) link_args = link
sources = [] sources = []
src += ['anim.c', 'config.c', 'datastream.c','font.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', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c'] 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_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.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'] curl_opts = [
'http=enabled',
'ssl=enabled',
'openssl=enabled',
'schannel=disabled',
'secure-transport=disabled',
'dict=disabled',
'file=disabled',
'ftp=disabled',
'gopher=disabled',
'imap=disabled',
'ldap=disabled',
'ldaps=disabled',
'mqtt=disabled',
'pop3=disabled',
'rtmp=disabled',
'rtsp=disabled',
'smb=disabled',
'smtp=disabled',
'telnet=disabled',
'tftp=disabled',
'alt-svc=disabled',
'asynchdns=disabled',
'aws=disabled',
'basic-auth=disabled',
'bearer-auth=disabled',
'bindlocal=disabled',
'brotli=disabled',
'cookies=disabled',
'digest-auth=disabled',
'doh=disabled',
'form-api=disabled',
'getoptions=disabled',
'gsasl=disabled',
'gss-api=disabled',
'headers-api=disabled',
'hsts=disabled',
'http2=disabled',
'idn=disabled',
'kerberos-auth=disabled',
'libcurl-option=disabled',
'libz=disabled',
'mime=disabled',
'negotiate-auth=disabled',
'netrc=disabled',
'ntlm=disabled',
'parsedate=disabled',
'progress-meter=disabled',
'proxy=disabled',
'psl=disabled',
'sha512_256=disabled',
'shuffle-dns=disabled',
'socketpair=disabled',
'tls-srp=disabled',
'unixsockets=disabled',
'verbose-strings=disabled',
'zstd=disabled',
'debug=disabled',
'curldebug=false',
'libuv=disabled',
'tests=disabled',
'unittests=disabled',
'default_library=static'
]
curl_proj = subproject('curl', default_options: curl_opts)
deps += dependency('libcurl')
deps += dependency('zlib', static: true)
deps += dependency('openssl', static:true)
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)
@@ -170,7 +263,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',
@@ -190,6 +283,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
@@ -198,7 +311,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',
@@ -212,7 +325,7 @@ prosperon = custom_target('prosperon',
) )
prosperon_dep = declare_dependency( prosperon_dep = declare_dependency(
link_with:prosperon link_with: prosperon
) )
copy_tests = custom_target( copy_tests = custom_target(
@@ -231,10 +344,13 @@ tests = [
'spawn_actor', 'spawn_actor',
'empty', 'empty',
'nota', 'nota',
'enet' '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

@@ -3,6 +3,7 @@ site_name: Prosperon Documentation
plugins: plugins:
- search - search
- awesome-pages - awesome-pages
- mike
extra_css: extra_css:
- style.css - style.css
@@ -31,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

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

@@ -0,0 +1,165 @@
$_.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.');
console.log('subscribing: ' + json.encode(e.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

@@ -64,28 +64,14 @@ Cmdline.register_order(
"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 // rm actor so it can't be tampered
globalThis.actor = undefined globalThis.actor = undefined
@@ -135,28 +121,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];
@@ -165,6 +129,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,15 +207,37 @@ Cmdline.register_order(
); );
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,
}

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 }
};

View File

@@ -3,123 +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) // [-π,π]
image:whiteimage 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,
@@ -127,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
@@ -156,33 +400,24 @@ 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,
}
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) { draw.images = function images(image, rects, config) {
if (!image) throw Error('Need an image to render.') if (!image) throw Error('Need an image to render.')
@@ -211,29 +446,40 @@ draw.images[prosperon.DOC] = `
:raises Error: If no image is provided. :raises Error: If no image is provided.
` `
draw.sprites = function(sprites, sort = 0, pipeline) { function software_circle(pos, radius)
var cmds = graphics.make_sprite_queue(sprites, prosperon.camera, pipeline, sort) {
for (var i = 0; i < cmds.length; i++) if (radius <= 0) return // nothing to draw
render.queue(cmds[i])
}
draw.sprites[prosperon.DOC] = `
:param sprites: An array of sprite objects to draw.
: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) { var cx = pos[0], cy = pos[1]
render.rectangle({x:pos.x, y:pos.y, width:radius*2,height:radius*2}, color, circle_pipeline) var x = 0, y = radius
var d = 3 - (radius << 1) // decision parameter
while (x <= y) {
draw.point([
[cx + x, cy + y], [cx - x, cy + y],
[cx + x, cy - y], [cx - x, cy - y],
[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 circle_def = {
inner_radius:1, // percentage: 1 means filled circle
color: Color.white,
start:0,
end: 1,
}
draw.circle = function render_circle(pos, radius, def, pipeline) {
draw.ellipse(pos, [radius,radius], def, 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)

View File

@@ -1,42 +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
var dmon = use('dmon') last_frame_time = os.now()
function dmon_cb(e) { prosperon.dispatch('dmon', e) }
function step() { function step() {
if (dmon)
dmon.poll(dmon_cb)
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()
@@ -45,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
} }

View File

@@ -1,7 +1,6 @@
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 ex = {} var ex = {}
@@ -111,7 +110,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

@@ -1,4 +1,4 @@
var ret = { return {
get pos() { get pos() {
if (!this.transform) return if (!this.transform) return
return this.transform.pos; return this.transform.pos;
@@ -29,5 +29,3 @@ var ret = {
this.scale = this.scale.map((x, i) => x * vec[i]); 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
@@ -7,49 +8,149 @@ rectangle packing, etc.
var io = use('io') var io = use('io')
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
function make_handle(obj)
{
var image = Object.create(graphics.Image);
if (obj.surface) {
im
}
return Object.assign(Object.create(graphics.Image), {
rect:{x:0,y:0,width:1,height:1},
[CPU]:obj,
[GPU]:undefined,
[LASTUSE]:os.now()
})
}
function wrapSurface(surf, maybeRect){
const h = make_handle(surf);
if(maybeRect) h.rect = maybeRect; /* honour frame sub-rect */
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 'ase':
case 'aseprite': case 'aseprite': return graphics.make_aseprite(bytes)
newimg = graphics.make_aseprite(data) default: return {surface:graphics.make_texture(bytes)}
if (newimg.surface)
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
else {
for (var anim in newimg) {
var a = newimg[anim]
for (var frame of a.frames)
frame.texture = prosperon.gpu.load_texture(frame.surface)
} }
}
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 );
} }
break if(Object.keys(anims).length) return anims;
default:
newimg = { throw new Error('Unsupported image structure from decoder');
surface: graphics.make_texture(data)
}catch(e){
console.error(`Error loading image ${path}: ${e.message}`);
throw e;
} }
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
break
}
return newimg
} }
var image = {} var image = {}
@@ -82,15 +183,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 +305,12 @@ 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]
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.
@@ -265,13 +403,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.

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

@@ -0,0 +1,26 @@
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.
`
http.poll[prosperon.DOC] = `Process pending HTTP requests and invoke callbacks.
This function checks for I/O activity on all enqueued HTTP requests and processes any that have completed or received data. For completed requests, it invokes the 'callback' function (if provided) with the result. For streaming requests, it invokes the 'data' function (if provided) as data arrives. This function must be called repeatedly to drive the asynchronous request system.
:return: undefined
:throws: An error if CURL multi-handle processing fails.
`
return http

View File

@@ -56,7 +56,7 @@ if (render_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");

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,106 @@
var sprite = {}
var graphics = use('graphics')
var render = use('render')
var draw2d = use('draw2d')
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] = graphics.make_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

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

@@ -0,0 +1,101 @@
/**
* Moth Game Framework
* Higher-level game development framework built on top of Prosperon
*/
var os = use('os');
var io = use('io');
var render = use('render');
var actor = use('actor');
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: os.make_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
};

View File

@@ -1,5 +1,25 @@
var nota = this 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. 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. 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.

View File

@@ -15,9 +15,6 @@ 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.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.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_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_voltage[prosperon.DOC] = "Return the current battery voltage in volts, if available."
@@ -25,7 +22,6 @@ os.battery_seconds[prosperon.DOC] = "Return the estimated remaining battery time
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.env[prosperon.DOC] = "Fetch the value of a given environment variable, or undefined if it doesn't exist."

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,842 +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: "game",
url: "https://prosperon.dev"
}
var config = use('config.js')
config.__proto__ = default_conf
prosperon.camera = use('ext/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 whiteimage = {}
whiteimage.surface = graphics.make_surface([1,1])
whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1])
whiteimage.texture = render._main.load_texture(whiteimage.surface)
var imgui = use('imgui')
if (imgui) imgui.init(render._main, prosperon.window)
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"
}
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 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) { // 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()
}
}
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
`
// 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

@@ -41,9 +41,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 +53,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
} }
} }

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

@@ -0,0 +1,791 @@
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 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 = os.make_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 = 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) {
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,122 @@
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"
}
prosperon.window = prosperon.engine_start({width:500,height:500})
context = prosperon.window.make_renderer("vulkan")
}
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;
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

@@ -306,7 +306,7 @@ struct ase_t
void* mem_ctx; void* mem_ctx;
}; };
const char *aseprite_GetError(); const char *aseprite_GetError(void);
#endif // CUTE_ASEPRITE_H #endif // CUTE_ASEPRITE_H
@@ -317,11 +317,11 @@ const char *aseprite_GetError();
#define ASEPRITE_ERROR_MAX 256 #define ASEPRITE_ERROR_MAX 256
char aseprite_error[ASEPRITE_ERROR_MAX] = {0}; char aseprite_error[ASEPRITE_ERROR_MAX] = {0};
const char *aseprite_GetError() { const char *aseprite_GetError(void) {
return aseprite_error; return aseprite_error;
} }
void aseprite_clear_error() { void aseprite_clear_error(void) {
aseprite_error[0] = 0; aseprite_error[0] = 0;
} }
@@ -726,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
{ {
@@ -740,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

@@ -891,9 +891,7 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
ev->skip = true; ev->skip = true;
break; break;
} else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) {
// in some cases, particularly when created files under sub directories // handle trailing slashes
// there can be two modify events for a single subdir one with trailing slash and one without
// remove trailing slash from both cases and test
int l1 = (int)strlen(ev->filepath); int l1 = (int)strlen(ev->filepath);
int l2 = (int)strlen(check_ev->filepath); int l2 = (int)strlen(check_ev->filepath);
if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0';
@@ -910,14 +908,12 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
for (j = i + 1; j < c && !loop_break; j++) { for (j = i + 1; j < c && !loop_break; j++) {
dmon__inotify_event* check_ev = &_dmon.events[j]; dmon__inotify_event* check_ev = &_dmon.events[j];
if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) {
// there is a case where some programs (like gedit): // check for rename sequences
// when we save, it creates a temp file, and moves it to the file being modified
// search for these cases and remove all of them
int k; int k;
for (k = j + 1; k < c; k++) { for (k = j + 1; k < c; k++) {
dmon__inotify_event* third_ev = &_dmon.events[k]; dmon__inotify_event* third_ev = &_dmon.events[k];
if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) {
third_ev->mask = IN_MODIFY; // change to modified third_ev->mask = IN_MODIFY; // treat as a modify
ev->skip = check_ev->skip = true; ev->skip = check_ev->skip = true;
loop_break = true; loop_break = true;
break; break;
@@ -925,7 +921,6 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
} }
} else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
// Another case is that file is copied. CREATE and MODIFY happens sequentially // Another case is that file is copied. CREATE and MODIFY happens sequentially
// so we ignore MODIFY event
check_ev->skip = true; check_ev->skip = true;
} }
} }
@@ -939,10 +934,7 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
break; break;
} }
} }
// If destination is not valid, treat as delete
// in some environments like nautilus file explorer:
// when a file is deleted, it is moved to recycle bin
// so if the destination of the move is not valid, it's probably DELETE
if (!move_valid) { if (!move_valid) {
ev->mask = IN_DELETE; ev->mask = IN_DELETE;
} }
@@ -956,10 +948,7 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
break; break;
} }
} }
// If source is not valid, treat as create
// in some environments like nautilus file explorer:
// when a file is deleted, it is moved to recycle bin, on undo it is moved back it
// so if the destination of the move is not valid, it's probably CREATE
if (!move_valid) { if (!move_valid) {
ev->mask = IN_CREATE; ev->mask = IN_CREATE;
} }
@@ -967,7 +956,6 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
int j; int j;
for (j = i + 1; j < c; j++) { for (j = i + 1; j < c; j++) {
dmon__inotify_event* check_ev = &_dmon.events[j]; dmon__inotify_event* check_ev = &_dmon.events[j];
// if the file is DELETED and then MODIFIED after, just ignore the modify event
if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
check_ev->skip = true; check_ev->skip = true;
break; break;
@@ -1009,10 +997,9 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
stb_sb_push(watch->subdirs, subdir); stb_sb_push(watch->subdirs, subdir);
stb_sb_push(watch->wds, wd); stb_sb_push(watch->wds, wd);
// some directories may be already created, for instance, with the command: mkdir -p // gather newly created subdirs
// so we will enumerate them manually and add them to the events
_dmon_gather_recursive(watch, watchdir); _dmon_gather_recursive(watch, watchdir);
ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated ev = &_dmon.events[i];
} }
} }
watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data);
@@ -1256,7 +1243,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
return _dmon_make_id(0); return _dmon_make_id(0);
} }
dmon__watch_subdir subdir; dmon__watch_subdir subdir;
_dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is a dummy entry
stb_sb_push(watch->subdirs, subdir); stb_sb_push(watch->subdirs, subdir);
stb_sb_push(watch->wds, wd); stb_sb_push(watch->wds, wd);
@@ -1369,7 +1356,7 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
continue; continue;
} }
// remove redundant modify events on a single file // remove redundant modifies on a single file
if (ev->event_flags & kFSEventStreamEventFlagItemModified) { if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
int j; int j;
for (j = i + 1; j < c; j++) { for (j = i + 1; j < c; j++) {
@@ -1380,7 +1367,8 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
break; break;
} }
} }
} else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { }
else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) {
int j; int j;
for (j = i + 1; j < c; j++) { for (j = i + 1; j < c; j++) {
dmon__fsevent_event* check_ev = &_dmon.events[j]; dmon__fsevent_event* check_ev = &_dmon.events[j];
@@ -1391,10 +1379,7 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
} }
} }
// in some environments like finder file explorer: // if not valid rename, treat as remove or create
// when a file is deleted, it is moved to recycle bin
// so if the destination of the move is not valid, it's probably DELETE or CREATE
// decide CREATE if file exists
if (!ev->move_valid) { if (!ev->move_valid) {
ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed;
@@ -1429,10 +1414,10 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL,
watch->user_data); watch->user_data);
} }
if (ev->event_flags & kFSEventStreamEventFlagItemModified) { if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, watch->user_data); watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, watch->user_data);
} else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { }
else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) {
int j; int j;
for (j = i + 1; j < c; j++) { for (j = i + 1; j < c; j++) {
dmon__fsevent_event* check_ev = &_dmon.events[j]; dmon__fsevent_event* check_ev = &_dmon.events[j];
@@ -1442,7 +1427,8 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
break; break;
} }
} }
} else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { }
else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) {
watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL,
watch->user_data); watch->user_data);
} }
@@ -1478,8 +1464,19 @@ _DMON_PRIVATE void* _dmon_thread(void* arg)
dmon__watch_state* watch = _dmon.watches[i]; dmon__watch_state* watch = _dmon.watches[i];
if (!watch->init) { if (!watch->init) {
DMON_ASSERT(watch->fsev_stream_ref); DMON_ASSERT(watch->fsev_stream_ref);
// Modified block: Use dispatch queue if macOS >= 13, else run loop
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 130000
{
dispatch_queue_t queue = dispatch_queue_create("com.dmon.fsevents", DISPATCH_QUEUE_SERIAL);
FSEventStreamSetDispatchQueue(watch->fsev_stream_ref, queue);
FSEventStreamStart(watch->fsev_stream_ref);
}
#else
{
FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode); FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode);
FSEventStreamStart(watch->fsev_stream_ref); FSEventStreamStart(watch->fsev_stream_ref);
}
#endif
watch->init = true; watch->init = true;
} }
@@ -1587,8 +1584,8 @@ _DMON_PRIVATE void _dmon_fsevent_callback(ConstFSEventStreamRef stream_ref, void
_dmon_strcpy(abs_filepath, sizeof(abs_filepath), filepath); _dmon_strcpy(abs_filepath, sizeof(abs_filepath), filepath);
_dmon_unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); _dmon_unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath);
// normalize path, so it would be the same on both MacOS file-system types (case/nocase) // normalize path for case-insensitive volumes
_dmon_tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); _dmon_tolower(abs_filepath_lower, sizeof(abs_filepath_lower), abs_filepath);
DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower);
// strip the root dir from the beginning // strip the root dir from the beginning
@@ -1707,7 +1704,6 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25,
kFSEventStreamCreateFlagFileEvents); kFSEventStreamCreateFlagFileEvents);
CFRelease(cf_dirarr); CFRelease(cf_dirarr);
CFRelease(cf_dir); CFRelease(cf_dir);

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
#ifndef FFI_H #ifndef FFI_H
#define FFI_H #define FFI_H
#include <quickjs.h> #include <quickjs.h>
void ffi_load(JSContext *js, int argc, char **argv); void ffi_load(JSContext *js);
int js_print_exception(JSContext *js, JSValue v); int js_print_exception(JSContext *js, JSValue v);
#endif #endif

2956
source/monocypher.c Normal file

File diff suppressed because it is too large Load Diff

321
source/monocypher.h Normal file
View File

@@ -0,0 +1,321 @@
// Monocypher version __git__
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef MONOCYPHER_H
#define MONOCYPHER_H
#include <stddef.h>
#include <stdint.h>
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#elif defined(__cplusplus)
extern "C" {
#endif
// Constant time comparisons
// -------------------------
// Return 0 if a and b are equal, -1 otherwise
int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
// Erase sensitive data
// --------------------
void crypto_wipe(void *secret, size_t size);
// Authenticated encryption
// ------------------------
void crypto_aead_lock(uint8_t *cipher_text,
uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_unlock(uint8_t *plain_text,
const uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// Authenticated stream
// --------------------
typedef struct {
uint64_t counter;
uint8_t key[32];
uint8_t nonce[8];
} crypto_aead_ctx;
void crypto_aead_init_x(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[24]);
void crypto_aead_init_djb(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[8]);
void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[12]);
void crypto_aead_write(crypto_aead_ctx *ctx,
uint8_t *cipher_text,
uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_read(crypto_aead_ctx *ctx,
uint8_t *plain_text,
const uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// General purpose hash (BLAKE2b)
// ------------------------------
// Direct interface
void crypto_blake2b(uint8_t *hash, size_t hash_size,
const uint8_t *message, size_t message_size);
void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
const uint8_t *key, size_t key_size,
const uint8_t *message, size_t message_size);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint64_t hash[8];
uint64_t input_offset[2];
uint64_t input[16];
size_t input_idx;
size_t hash_size;
} crypto_blake2b_ctx;
void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
const uint8_t *key, size_t key_size);
void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
// Password key derivation (Argon2)
// --------------------------------
#define CRYPTO_ARGON2_D 0
#define CRYPTO_ARGON2_I 1
#define CRYPTO_ARGON2_ID 2
typedef struct {
uint32_t algorithm; // Argon2d, Argon2i, Argon2id
uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
uint32_t nb_lanes; // parallelism level (single threaded anyway)
} crypto_argon2_config;
typedef struct {
const uint8_t *pass;
const uint8_t *salt;
uint32_t pass_size;
uint32_t salt_size; // 16 bytes recommended
} crypto_argon2_inputs;
typedef struct {
const uint8_t *key; // may be NULL if no key
const uint8_t *ad; // may be NULL if no additional data
uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
uint32_t ad_size; // 0 if no additional data
} crypto_argon2_extras;
extern const crypto_argon2_extras crypto_argon2_no_extras;
void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
crypto_argon2_config config,
crypto_argon2_inputs inputs,
crypto_argon2_extras extras);
// Key exchange (X-25519)
// ----------------------
// Shared secrets are not quite random.
// Hash them to derive an actual shared key.
void crypto_x25519_public_key(uint8_t public_key[32],
const uint8_t secret_key[32]);
void crypto_x25519(uint8_t raw_shared_secret[32],
const uint8_t your_secret_key [32],
const uint8_t their_public_key [32]);
// Conversion to EdDSA
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
// scalar "division"
// Used for OPRF. Be aware that exponential blinding is less secure
// than Diffie-Hellman key exchange.
void crypto_x25519_inverse(uint8_t blind_salt [32],
const uint8_t private_key[32],
const uint8_t curve_point[32]);
// "Dirty" versions of x25519_public_key().
// Use with crypto_elligator_rev().
// Leaks 3 bits of the private key.
void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
// Signatures
// ----------
// EdDSA with curve25519 + BLAKE2b
void crypto_eddsa_key_pair(uint8_t secret_key[64],
uint8_t public_key[32],
uint8_t seed[32]);
void crypto_eddsa_sign(uint8_t signature [64],
const uint8_t secret_key[64],
const uint8_t *message, size_t message_size);
int crypto_eddsa_check(const uint8_t signature [64],
const uint8_t public_key[32],
const uint8_t *message, size_t message_size);
// Conversion to X25519
void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
// EdDSA building blocks
void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
void crypto_eddsa_mul_add(uint8_t r[32],
const uint8_t a[32],
const uint8_t b[32],
const uint8_t c[32]);
void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
int crypto_eddsa_check_equation(const uint8_t signature[64],
const uint8_t public_key[32],
const uint8_t h_ram[32]);
// Chacha20
// --------
// Specialised hash.
// Used to hash X25519 shared secrets.
void crypto_chacha20_h(uint8_t out[32],
const uint8_t key[32],
const uint8_t in [16]);
// Unauthenticated stream cipher.
// Don't forget to add authentication.
uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[8],
uint64_t ctr);
uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[12],
uint32_t ctr);
uint64_t crypto_chacha20_x(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[24],
uint64_t ctr);
// Poly 1305
// ---------
// This is a *one time* authenticator.
// Disclosing the mac reveals the key.
// See crypto_lock() on how to use it properly.
// Direct interface
void crypto_poly1305(uint8_t mac[16],
const uint8_t *message, size_t message_size,
const uint8_t key[32]);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint8_t c[16]; // chunk of the message
size_t c_idx; // How many bytes are there in the chunk.
uint32_t r [4]; // constant multiplier (from the secret key)
uint32_t pad[4]; // random number added at the end (from the secret key)
uint32_t h [5]; // accumulated hash
} crypto_poly1305_ctx;
void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
// Elligator 2
// -----------
// Elligator mappings proper
void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
uint8_t tweak);
// Easy to use key pair generation
void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
uint8_t seed[32]);
#ifdef __cplusplus
}
#endif
#endif // MONOCYPHER_H

View File

@@ -29,7 +29,7 @@
#define NOTA_EXP_SIGN(CHAR) (CHAR & (1<<4)) #define NOTA_EXP_SIGN(CHAR) (CHAR & (1<<4))
#define NOTA_TYPE 0x70 #define NOTA_TYPE 0x70
#define NOTA_HEAD_DATA 0x0f #define NOTA_HEAD_DATA 0x0f
#define CONTINUE(CHAR) ((CHAR)>>7) #define CONTINUE(CHAR) (CHAR>>7)
#define UTF8_DATA 0x3f #define UTF8_DATA 0x3f
/* A helper to get the high-level Nota type nibble from a byte */ /* A helper to get the high-level Nota type nibble from a byte */

File diff suppressed because it is too large Load Diff

91
source/prosperon.h Normal file
View File

@@ -0,0 +1,91 @@
#ifndef PROSPERON_H
#define PROSPERON_H
#include <SDL3/SDL.h>
#include "quickjs.h"
#define STATE_VECTOR_LENGTH 624
#define STATE_VECTOR_M 397
#define ACTOR_IDLE 0
#define ACTOR_READY 1
#define ACTOR_RUNNING 2
#define ACTOR_EXHAUSTED 3
#define ACTOR_RECLAIMING 4
#define ACTOR_SLOW 5
typedef JSValue (*MODULEFN)(JSContext *js);
typedef struct tagMTRand {
uint32_t mt[STATE_VECTOR_LENGTH];
int32_t index;
} MTRand;
typedef struct {
const char *name;
MODULEFN fn;
} ModuleEntry;
typedef struct {
int argc;
char **argv;
} cmdargs;
typedef struct prosperon_rt {
cmdargs cmd;
JSContext *context;
JSValue cycle_fn;
JSValue idx_buffer;
JSValue on_exception;
JSValue message_handle;
JSValue unneeded;
ModuleEntry *module_registry;
JSValue *js_swapchains;
/* Protects JSContext usage */
SDL_Mutex *mutex;
SDL_Mutex *turn;
char *id;
MTRand mrand;
double unneeded_secs;
int idx_count;
/* The “mailbox” for incoming messages + a dedicated lock for it: */
void **messages;
JSValue *events;
SDL_Mutex *msg_mutex; /* For messages queue only */
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
struct { Uint32 key; JSValue value; } *timers;
int state;
Uint32 ar;
int need_stop;
int main_thread_only;
} prosperon_rt;
extern SDL_ThreadID main_thread;
extern SDL_TLSID prosperon_id;
prosperon_rt *create_actor(int argc, char **argv);
const char *register_actor(const char *id, prosperon_rt *actor, int mainthread);
void actor_free(prosperon_rt *actor);
const char *send_message(const char *id, void *msg);
Uint32 actor_timer_cb(prosperon_rt *actor, SDL_TimerID id, Uint32 interval);
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv);
JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv);
void script_startup(prosperon_rt *rt);
void script_evalf(JSContext *js, const char *format, ...);
JSValue script_eval(JSContext *js, const char *file, const char *script);
int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id);
void set_actor_state(prosperon_rt *actor);
int prosperon_mount_core(void);
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv);
#endif

460
source/qjs_blob.c Normal file
View File

@@ -0,0 +1,460 @@
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "quickjs.h"
#include "qjs_blob.h"
// -----------------------------------------------------------------------------
// A simple blob structure that can be in two states:
// - antestone (mutable): writing is allowed
// - stone (immutable): reading is allowed
//
// The blob is stored as an array of bits in memory, but for simplicity here,
// we store them in a dynamic byte array with a bit_length and capacity in bits.
//
// This is a minimal demonstration. Real usage might require more sophisticated
// memory or bit manipulation for performance.
// -----------------------------------------------------------------------------
typedef struct {
// The actual buffer holding the bits (in multiples of 8 bits).
uint8_t *data;
// The total number of bits currently in use (the "length" of the blob).
size_t bit_length;
// The total capacity in bits that 'data' can currently hold without realloc.
size_t bit_capacity;
// 0 = antestone (mutable)
// 1 = stone (immutable)
int is_stone;
} JSBlobData;
// Forward declaration of class ID and methods
static JSClassID js_blob_class_id;
// Helper to ensure capacity for writing
// new_bits is additional bits to be appended
static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bits) {
size_t need_bits = bd->bit_length + new_bits;
if (need_bits <= bd->bit_capacity) return 0;
// Increase capacity (in multiples of bytes).
// We can pick a growth strategy. For demonstration, double it:
size_t new_capacity = bd->bit_capacity == 0 ? 64 : bd->bit_capacity * 2;
while (new_capacity < need_bits) new_capacity *= 2;
// Round up new_capacity to a multiple of 8 bits
if (new_capacity % 8) {
new_capacity += 8 - (new_capacity % 8);
}
size_t new_size_bytes = new_capacity / 8;
uint8_t *new_ptr = realloc(bd->data, new_size_bytes);
if (!new_ptr) {
return -1; // out of memory
}
// zero-fill the new area (only beyond the old capacity)
size_t old_size_bytes = bd->bit_capacity / 8;
if (new_size_bytes > old_size_bytes) {
memset(new_ptr + old_size_bytes, 0, new_size_bytes - old_size_bytes);
}
bd->data = new_ptr;
bd->bit_capacity = new_capacity;
return 0;
}
// Finalizer for JSBlobData
static void js_blob_finalizer(JSRuntime *rt, JSValue val) {
JSBlobData *bd = JS_GetOpaque(val, js_blob_class_id);
if (bd) {
free(bd->data);
bd->data = NULL;
bd->bit_length = 0;
bd->bit_capacity = 0;
bd->is_stone = 0;
free(bd);
}
}
// Mark function: not used here, as we have no child JS objects in JSBlobData
static void js_blob_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) {
// No child JS references to mark
}
// A helper to create a new JSBlobData object, returning a JSValue wrapping it.
static JSValue js_blob_wrap(JSContext *ctx, JSBlobData *bd) {
JSValue obj = JS_NewObjectClass(ctx, js_blob_class_id);
if (JS_IsException(obj)) {
free(bd->data);
free(bd);
return obj;
}
JS_SetOpaque(obj, bd);
return obj;
}
// -----------------------------------------------------------------------------
// Helpers for reading/writing bits
// -----------------------------------------------------------------------------
// Write one bit (0 or 1) at the end of the blob
static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_val) {
if (bd->is_stone) {
// Trying to write to an immutable blob -> throw
return -1;
}
if (js_blob_ensure_capacity(ctx, bd, 1) < 0) {
return -1;
}
// index in bits
size_t bit_index = bd->bit_length;
size_t byte_index = bit_index >> 3;
size_t offset_in_byte = bit_index & 7;
// set or clear bit
if (bit_val)
bd->data[byte_index] |= (1 << offset_in_byte);
else
bd->data[byte_index] &= ~(1 << offset_in_byte);
bd->bit_length++;
return 0;
}
// Read one bit from a stone blob at position 'pos'
static int js_blob_read_bit_internal(JSBlobData *bd, size_t pos, int *out_bit) {
if (!bd->is_stone) {
// It's not stone -> reading might be out of the specification
// but we can allow or return error. Here we just return error.
return -1;
}
if (pos >= bd->bit_length) {
return -1; // out of range
}
size_t byte_index = pos >> 3;
size_t offset_in_byte = pos & 7;
*out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0;
return 0;
}
// Turn a blob into the "stone" state. This discards any extra capacity.
static void js_blob_make_stone(JSBlobData *bd) {
bd->is_stone = 1;
// Optionally shrink the buffer to exactly bit_length in size
if (bd->bit_capacity > bd->bit_length) {
size_t size_in_bytes = (bd->bit_length + 7) >> 3; // round up to full bytes
uint8_t *new_ptr = NULL;
if (size_in_bytes) {
new_ptr = realloc(bd->data, size_in_bytes);
if (new_ptr) {
bd->data = new_ptr;
}
} else {
// zero length
free(bd->data);
bd->data = NULL;
}
bd->bit_capacity = bd->bit_length; // capacity in bits now matches length
}
}
// -----------------------------------------------------------------------------
// JS Functions (blob.make, blob.write_bit, blob.read_logical, etc.)
// -----------------------------------------------------------------------------
// blob.make(...)
static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
// We'll implement a few typical signatures:
// blob.make()
// blob.make(capacity)
// blob.make(length, logical_value)
// blob.make(blob, from, to) (makes a copy)
//
// This is a simplified approach. The spec mentions random, partial copy, etc.
// We'll handle just these forms enough to demonstrate the concept.
JSBlobData *bd = calloc(1, sizeof(*bd));
if (!bd) return JS_ThrowOutOfMemory(ctx);
// default
bd->data = NULL;
bd->bit_length = 0;
bd->bit_capacity = 0;
bd->is_stone = 0; // initially antestone
// blob.make()
if (argc == 0) {
// empty antestone blob
}
// blob.make(capacity)
else if (argc == 1 && JS_IsNumber(argv[0])) {
int64_t capacity_bits;
if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) {
free(bd);
return JS_EXCEPTION;
}
if (capacity_bits < 0) capacity_bits = 0;
bd->bit_capacity = (size_t)capacity_bits;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
if (bd->bit_capacity) {
size_t bytes = bd->bit_capacity / 8;
bd->data = calloc(bytes, 1);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
}
}
// blob.make(length, logical)
else if (argc == 2 && JS_IsNumber(argv[0]) && JS_IsBool(argv[1])) {
int64_t length_bits;
if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) {
free(bd);
return JS_EXCEPTION;
}
if (length_bits < 0) length_bits = 0;
int is_one = JS_ToBool(ctx, argv[1]);
bd->bit_length = (size_t)length_bits;
bd->bit_capacity = bd->bit_length;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
size_t bytes = bd->bit_capacity / 8;
if (bytes) {
bd->data = malloc(bytes);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
memset(bd->data, is_one ? 0xff : 0x00, bytes);
// if length_bits isn't a multiple of 8, we need to clear the unused bits
size_t used_bits_in_last_byte = (size_t)length_bits & 7;
if (used_bits_in_last_byte && is_one) {
// clear top bits in the last byte
uint8_t mask = (1 << used_bits_in_last_byte) - 1;
bd->data[bytes - 1] &= mask;
}
}
}
// blob.make(blob, from, to)
else if (argc >= 1 && JS_IsObject(argv[0])) {
// we try copying from another blob if it's of the same class
JSBlobData *src = JS_GetOpaque(argv[0], js_blob_class_id);
if (!src) {
free(bd);
return JS_ThrowTypeError(ctx, "blob.make: argument 1 not a blob");
}
int64_t from = 0, to = (int64_t)src->bit_length;
if (argc >= 2 && JS_IsNumber(argv[1])) {
JS_ToInt64(ctx, &from, argv[1]);
if (from < 0) from = 0;
}
if (argc >= 3 && JS_IsNumber(argv[2])) {
JS_ToInt64(ctx, &to, argv[2]);
if (to < from) to = from;
if (to > (int64_t)src->bit_length) to = (int64_t)src->bit_length;
}
size_t copy_len = (size_t)(to - from);
bd->bit_length = copy_len;
bd->bit_capacity = copy_len;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
size_t bytes = bd->bit_capacity / 8;
if (bytes) {
bd->data = calloc(bytes, 1);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
}
// Now copy the bits.
// For simplicity, let's do a naive bit copy one by one:
for (size_t i = 0; i < copy_len; i++) {
size_t src_bit_index = from + i;
size_t src_byte = src_bit_index >> 3;
size_t src_off = src_bit_index & 7;
int bit_val = (src->data[src_byte] >> src_off) & 1;
size_t dst_byte = i >> 3;
size_t dst_off = i & 7;
if (bit_val) {
bd->data[dst_byte] |= (1 << dst_off);
} else {
bd->data[dst_byte] &= ~(1 << dst_off);
}
}
}
// else fail
else {
free(bd);
return JS_ThrowTypeError(ctx, "blob.make: invalid arguments");
}
return js_blob_wrap(ctx, bd);
}
// blob.write_bit(blob, logical)
static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "blob.write_bit(blob, logical) requires 2 arguments");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.write_bit: argument 1 not a blob");
}
int bit_val = JS_ToBool(ctx, argv[1]); // interpret any truthy as 1, else 0
if (js_blob_write_bit_internal(ctx, bd, bit_val) < 0) {
return JS_ThrowTypeError(ctx, "blob.write_bit: cannot write (maybe stone or OOM)");
}
return JS_UNDEFINED;
}
// blob.read_logical(blob, from)
static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "blob.read_logical(blob, from) requires 2 arguments");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.read_logical: argument 1 not a blob");
}
int64_t pos;
if (JS_ToInt64(ctx, &pos, argv[1]) < 0) {
return JS_EXCEPTION;
}
if (pos < 0) {
return JS_NULL; // out of range
}
int bit_val;
if (js_blob_read_bit_internal(bd, (size_t)pos, &bit_val) < 0) {
return JS_NULL; // error or out of range
}
return JS_NewBool(ctx, bit_val);
}
// blob.stone(blob)
static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "blob.stone(blob) requires 1 argument");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.stone: argument not a blob");
}
if (!bd->is_stone) {
js_blob_make_stone(bd);
}
return JS_UNDEFINED;
}
// blob.length(blob)
// Return number of bits in the blob
static JSValue js_blob_length(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "blob.length(blob) requires 1 argument");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.length: argument not a blob");
}
return JS_NewInt64(ctx, bd->bit_length);
}
// blob.blob?(value)
// Return true if the value is a blob object
static JSValue js_blob_is_blob(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) return JS_FALSE;
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
return JS_NewBool(ctx, bd != NULL);
}
// -----------------------------------------------------------------------------
// Exports list
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_blob_funcs[] = {
// The "make" function.
JS_CFUNC_DEF("make", 3, js_blob_make),
// Some example read/write routines
JS_CFUNC_DEF("write_bit", 2, js_blob_write_bit),
JS_CFUNC_DEF("read_logical", 2, js_blob_read_logical),
// Convert blob from antestone -> stone
JS_CFUNC_DEF("stone", 1, js_blob_stone),
// Return the length in bits
JS_CFUNC_DEF("length", 1, js_blob_length),
// Check if a value is a blob
JS_CFUNC_DEF("isblob", 1, js_blob_is_blob),
};
// -----------------------------------------------------------------------------
// Class definition for the 'blob' objects
// -----------------------------------------------------------------------------
static JSClassDef js_blob_class = {
"BlobClass",
.finalizer = js_blob_finalizer,
.gc_mark = js_blob_mark,
};
// Module init function
static int js_blob_init(JSContext *ctx, JSModuleDef *m) {
// Register the class if not already done
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
JS_NewClassID(&js_blob_class_id);
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
}
// Create a prototype object
JSValue proto = JS_NewObject(ctx);
JS_SetClassProto(ctx, js_blob_class_id, proto);
// Export our functions as named exports
JS_SetModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return 0;
}
// The module entry point
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_blob
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_blob_init);
if (!m) return NULL;
JS_AddModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return m;
}
// -----------------------------------------------------------------------------
// js_blob_use(ctx) for easy embedding: returns an object with the blob functions
// -----------------------------------------------------------------------------
JSValue js_blob_use(JSContext *ctx) {
// Ensure class is registered
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
JS_NewClassID(&js_blob_class_id);
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
// Create a prototype object
JSValue proto = JS_NewObject(ctx);
JS_SetClassProto(ctx, js_blob_class_id, proto);
}
// Create a plain object (the "exports") and add the funcs
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, obj, js_blob_funcs,
sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return obj;
}

8
source/qjs_blob.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_BLOB_H
#define QJS_BLOB_H
#include <quickjs.h>
JSValue js_blob_use(JSContext *ctx);
#endif

213
source/qjs_crypto.c Normal file
View File

@@ -0,0 +1,213 @@
#include "qjs_crypto.h"
#include "quickjs.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "monocypher.h"
#include <stdint.h>
#include <stddef.h>
#if defined(_WIN32)
// ------- Windows: use BCryptGenRandom -------
#include <windows.h>
#include <bcrypt.h>
int randombytes(void *buf, size_t n) {
NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
return (status == 0) ? 0 : -1;
}
#elif defined(__linux__)
// ------- Linux: try getrandom, fall back to /dev/urandom -------
#include <unistd.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <errno.h>
// If we have a new enough libc and kernel, getrandom is available.
// Otherwise, well do a /dev/urandom fallback.
#include <sys/stat.h>
static int randombytes_fallback(void *buf, size_t n) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
ssize_t r = read(fd, buf, n);
close(fd);
return (r == (ssize_t)n) ? 0 : -1;
}
int randombytes(void *buf, size_t n) {
#ifdef SYS_getrandom
// Try getrandom(2) if available
ssize_t ret = syscall(SYS_getrandom, buf, n, 0);
if (ret < 0) {
// If getrandom is not supported or fails, fall back
if (errno == ENOSYS) {
return randombytes_fallback(buf, n);
}
return -1;
}
return (ret == (ssize_t)n) ? 0 : -1;
#else
// getrandom not available, just fallback
return randombytes_fallback(buf, n);
#endif
}
#else
// ------- Other Unix: read from /dev/urandom -------
#include <fcntl.h>
#include <unistd.h>
int randombytes(void *buf, size_t n) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
ssize_t r = read(fd, buf, n);
close(fd);
return (r == (ssize_t)n) ? 0 : -1;
}
#endif
static inline void to_hex(const uint8_t *in, size_t in_len, char *out)
{
static const char hexchars[] = "0123456789abcdef";
for (size_t i = 0; i < in_len; i++) {
out[2*i ] = hexchars[(in[i] >> 4) & 0x0F];
out[2*i + 1] = hexchars[ in[i] & 0x0F];
}
out[2 * in_len] = '\0'; // null-terminate
}
static inline int nibble_from_char(char c, uint8_t *nibble)
{
if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0'); return 0; }
if (c >= 'a' && c <= 'f') { *nibble = (uint8_t)(c - 'a' + 10); return 0; }
if (c >= 'A' && c <= 'F') { *nibble = (uint8_t)(c - 'A' + 10); return 0; }
return -1; // invalid char
}
static inline int from_hex(const char *hex, uint8_t *out, size_t out_len)
{
for (size_t i = 0; i < out_len; i++) {
uint8_t hi, lo;
if (nibble_from_char(hex[2*i], &hi) < 0) return -1;
if (nibble_from_char(hex[2*i + 1], &lo) < 0) return -1;
out[i] = (uint8_t)((hi << 4) | lo);
}
return 0;
}
// Convert a JSValue containing a 64-character hex string into a 32-byte array.
static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto)
{
size_t hex_len;
const char *hex_str = JS_ToCStringLen(js, &hex_len, v);
if (!hex_str)
return;
if (hex_len != 64) {
JS_FreeCString(js, hex_str);
JS_ThrowTypeError(js, "js2crypto: expected 64-hex-char string");
return;
}
if (from_hex(hex_str, crypto, 32) < 0) {
JS_FreeCString(js, hex_str);
JS_ThrowTypeError(js, "js2crypto: invalid hex encoding");
return;
}
JS_FreeCString(js, hex_str);
}
static inline JSValue crypto2js(JSContext *js, const uint8_t *crypto)
{
char hex[65]; // 32*2 + 1 for null terminator
to_hex(crypto, 32, hex);
return JS_NewString(js, hex);
}
JSValue js_crypto_keypair(JSContext *js, JSValue self, int argc, JSValue *argv) {
JSValue ret = JS_NewObject(js);
uint8_t public[32];
uint8_t private[32];
randombytes(private,32);
private[0] &= 248;
private[31] &= 127;
private[31] |= 64;
crypto_x25519_public_key(public,private);
JS_SetPropertyStr(js, ret, "public", crypto2js(js, public));
JS_SetPropertyStr(js, ret, "private", crypto2js(js,private));
return ret;
}
JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1 || !JS_IsObject(argv[0])) {
return JS_ThrowTypeError(js, "crypto.shared: expected an object argument");
}
JSValue obj = argv[0];
JSValue val_pub = JS_GetPropertyStr(js, obj, "public");
if (JS_IsException(val_pub)) {
JS_FreeValue(js, val_pub);
return JS_EXCEPTION;
}
JSValue val_priv = JS_GetPropertyStr(js, obj, "private");
if (JS_IsException(val_priv)) {
JS_FreeValue(js, val_pub);
JS_FreeValue(js, val_priv);
return JS_EXCEPTION;
}
uint8_t pub[32], priv[32];
js2crypto(js, val_pub, pub);
js2crypto(js, val_priv, priv);
JS_FreeValue(js, val_pub);
JS_FreeValue(js, val_priv);
uint8_t shared[32];
crypto_x25519(shared, priv, pub);
return crypto2js(js, shared);
}
JSValue js_crypto_random(JSContext *js, JSValue self, int argc, JSValue *argv)
{
// 1) Pull 64 bits of cryptographically secure randomness
uint64_t r;
if (randombytes(&r, sizeof(r)) != 0) {
// If something fails (extremely rare), throw an error
return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes");
}
// 2) Convert r to a double in the range [0,1).
// We divide by (UINT64_MAX + 1.0) to ensure we never produce exactly 1.0.
double val = (double)r / ((double)UINT64_MAX + 1.0);
// 3) Return that as a JavaScript number
return JS_NewFloat64(js, val);
}
static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("keypair", 0, js_crypto_keypair),
JS_CFUNC_DEF("shared", 1, js_crypto_shared),
JS_CFUNC_DEF("random", 0, js_crypto_random),
};
JSValue js_crypto_use(JSContext *js)
{
JSValue obj = JS_NewObject(js);
JS_SetPropertyFunctionList(js, obj, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
return obj;
}

8
source/qjs_crypto.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_CRYPTO_H
#define QJS_CRYPTO_H
#include "quickjs.h"
JSValue js_crypto_use(JSContext *ctx);
#endif

View File

@@ -3,6 +3,8 @@
#include <enet/enet.h> #include <enet/enet.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <math.h>
#include "prosperon.h"
#define countof(a) (sizeof(a)/sizeof(*(a))) #define countof(a) (sizeof(a)/sizeof(*(a)))
@@ -10,77 +12,99 @@ static JSClassID enet_host_id;
static JSClassID enet_peer_class_id; static JSClassID enet_peer_class_id;
/* Finalizers */ /* Finalizers */
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) { static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
{
ENetHost *host = JS_GetOpaque(val, enet_host_id); ENetHost *host = JS_GetOpaque(val, enet_host_id);
if (host) { if (host) enet_host_destroy(host);
enet_host_destroy(host);
}
} }
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val) { static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
{
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
// No explicit cleanup needed for ENetPeer itself JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
(void)peer; }
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val)
{
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
JS_FreeValueRT(rt, *(JSValue*)peer->data);
free(peer->data);
} }
/* ENet init/deinit */ /* ENet init/deinit */
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
if (enet_initialize() != 0) { if (enet_initialize() != 0) return JS_ThrowInternalError(ctx, "Error initializing ENet");
return JS_ThrowInternalError(ctx, "Error initializing ENet.");
}
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
enet_deinitialize(); enet_deinitialize();
return JS_UNDEFINED; return JS_UNDEFINED;
} }
/* Host creation */ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, {
int argc, JSValueConst *argv) {
ENetHost *host; ENetHost *host;
ENetAddress address; ENetAddress address;
ENetAddress *send = &address;
size_t peer_count = 1000;
size_t channel_limit = 0;
enet_uint32 incoming_bandwidth = 0;
enet_uint32 outgoing_bandwidth = 0;
JSValue obj; JSValue obj;
if (argc < 1) { if (argc < 1 || !JS_IsObject(argv[0])) {
// Create client-like host, unbound host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
host = enet_host_create(NULL, 32, 2, 0, 0); if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet client host");
if (!host) { goto wrap;
return JS_ThrowInternalError(ctx, "Failed to create ENet host (null address).");
}
goto RET;
} }
// If arg is provided, interpret as "ip:port" for server JSValue config_obj = argv[0];
const char *address_str = JS_ToCString(ctx, argv[0]); JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
if (!address_str) { const char *addr_str = JS_IsString(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
return JS_EXCEPTION; // memory or conversion error JS_FreeValue(ctx, addr_val);
}
char ip[64];
int port;
if (sscanf(address_str, "%63[^:]:%d", ip, &port) != 2) { if (!addr_str)
JS_FreeCString(ctx, address_str); send = NULL;
return JS_ThrowTypeError(ctx, "Invalid address format. Expected 'ip:port'."); else {
} JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port");
JS_FreeCString(ctx, address_str); int32_t port32 = 0;
JS_ToInt32(ctx, &port32, port_val);
JS_FreeValue(ctx, port_val);
int err = enet_address_set_host_ip(&address, ip); if (strcmp(addr_str, "any") == 0)
address.host = ENET_HOST_ANY;
else if (strcmp(addr_str, "broadcast") == 0)
address.host = ENET_HOST_BROADCAST;
else {
int err = enet_address_set_host_ip(&address, addr_str);
if (err != 0) { if (err != 0) {
return JS_ThrowInternalError(ctx, "Failed to set host IP from %s. Error %d.", ip, err); JS_FreeCString(ctx, addr_str);
return JS_ThrowInternalError(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err);
} }
address.port = port; }
address.port = (enet_uint16)port32;
// Create server host with max 32 clients, 2 channels JS_FreeCString(ctx, addr_str);
host = enet_host_create(&address, 32, 2, 0, 0);
if (!host) {
return JS_ThrowInternalError(ctx, "Failed to create ENet host.");
} }
RET: JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels");
JS_ToUint32(ctx, &channel_limit, chan_val);
JS_FreeValue(ctx, chan_val);
JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth");
JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val);
JS_FreeValue(ctx, in_bw_val);
JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth");
JS_ToUint32(ctx, &outgoing_bandwidth, out_bw_val);
JS_FreeValue(ctx, out_bw_val);
host = enet_host_create(send, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet host");
wrap:
obj = JS_NewObjectClass(ctx, enet_host_id); obj = JS_NewObjectClass(ctx, enet_host_id);
if (JS_IsException(obj)) { if (JS_IsException(obj)) {
enet_host_destroy(host); enet_host_destroy(host);
@@ -90,82 +114,48 @@ RET:
return obj; return obj;
} }
static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer)
{
if (!peer->data) {
peer->data = malloc(sizeof(JSValue));
*(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id);
JS_SetOpaque(*(JSValue*)peer->data, peer);
}
return JS_DupValue(ctx, *(JSValue*)peer->data);
}
/* Host service: poll for events */ /* Host service: poll for events */
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id); ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) { if (!host) return JS_EXCEPTION;
return JS_EXCEPTION;
}
// Expect a callback function if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument");
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) { JSValue callback = JS_DupValue(ctx, argv[0]);
return JS_ThrowTypeError(ctx, "Expected a callback function as first argument.");
}
JSValue callback = argv[0];
JS_DupValue(ctx, callback);
// Optional timeout double secs;
int timeout = 0; JS_ToFloat64(ctx, &secs, argv[1]);
if (argc > 1) {
JS_ToInt32(ctx, &timeout, argv[1]);
}
ENetEvent event; ENetEvent event;
while (enet_host_service(host, &event, timeout) > 0) { while (enet_host_service(host, &event, secs*1000.0f) > 0) {
JSValue event_obj = JS_NewObject(ctx); JSValue event_obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, event_obj, "peer", peer_get_value(ctx, event.peer));
switch (event.type) { switch (event.type) {
case ENET_EVENT_TYPE_CONNECT: { case ENET_EVENT_TYPE_CONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect")); JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id);
if (JS_IsException(peer_obj)) {
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
return peer_obj;
}
JS_SetOpaque(peer_obj, event.peer);
JS_SetPropertyStr(ctx, event_obj, "peer", peer_obj);
break; break;
} case ENET_EVENT_TYPE_RECEIVE:
case ENET_EVENT_TYPE_RECEIVE: {
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive")); JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID)); JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
char *tmp = js_mallocz(ctx, event.packet->dataLength+1);
memcpy(tmp, event.packet->data, event.packet->dataLength);
tmp[event.packet->dataLength] = '\0';
// We expect strictly a JSON object // Pass raw data as string or ArrayBuffer
JSValue packet_data = JS_ParseJSON(ctx, if (event.packet->dataLength > 0) {
tmp, JSValue data_val = JS_NewArrayBufferCopy(ctx, event.packet->data, event.packet->dataLength);
event.packet->dataLength, JS_SetPropertyStr(ctx, event_obj, "data", data_val);
"<enet-packet>");
js_free(ctx,tmp);
if (JS_IsException(packet_data)) {
// Malformed JSON -> throw error, abort
printf("INVALID JSON!\n");
enet_packet_destroy(event.packet);
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
return JS_ThrowTypeError(ctx, "Received invalid JSON (parse error).");
} }
if (!JS_IsObject(packet_data)) {
// It might be a string/number/array/... -> we want only a plain object
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
JS_FreeValue(ctx, packet_data);
enet_packet_destroy(event.packet);
return JS_ThrowTypeError(ctx,
"Received data is not an object (must send a plain object).");
}
JS_SetPropertyStr(ctx, event_obj, "data", packet_data);
enet_packet_destroy(event.packet); enet_packet_destroy(event.packet);
break; break;
}
case ENET_EVENT_TYPE_DISCONNECT: case ENET_EVENT_TYPE_DISCONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect")); JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
break; break;
@@ -174,8 +164,7 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val,
break; break;
} }
// Invoke callback uncaught_exception(ctx, JS_Call(ctx, callback, JS_UNDEFINED, 1, &event_obj));
JS_Call(ctx, callback, JS_UNDEFINED, 1, &event_obj);
JS_FreeValue(ctx, event_obj); JS_FreeValue(ctx, event_obj);
} }
@@ -184,21 +173,15 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val,
} }
/* Host connect: client -> connect to server */ /* Host connect: client -> connect to server */
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id); ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) { if (!host) return JS_EXCEPTION;
return JS_EXCEPTION;
}
if (argc < 2) { if (argc < 2) return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port");
return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port.");
}
const char *hostname = JS_ToCString(ctx, argv[0]); const char *hostname = JS_ToCString(ctx, argv[0]);
if (!hostname) { if (!hostname) return JS_EXCEPTION;
return JS_EXCEPTION; // out of memory or conversion error
}
int port; int port;
JS_ToInt32(ctx, &port, argv[1]); JS_ToInt32(ctx, &port, argv[1]);
@@ -208,199 +191,166 @@ static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val,
address.port = port; address.port = port;
ENetPeer *peer = enet_host_connect(host, &address, 2, 0); ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
if (!peer) { if (!peer) return JS_ThrowInternalError(ctx, "No available peers for initiating an ENet connection");
return JS_ThrowInternalError(ctx, "Failed to initiate connection.");
}
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id); return peer_get_value(ctx, peer);
if (JS_IsException(peer_obj)) {
return peer_obj;
}
JS_SetOpaque(peer_obj, peer);
return peer_obj;
} }
/* Flush queued packets */ /* Flush queued packets */
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id); ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) { if (!host) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_host_flush(host); enet_host_flush(host);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
/* Broadcast a plain object */ /* Broadcast a string or ArrayBuffer */
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id); ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) { if (!host) return JS_EXCEPTION;
return JS_EXCEPTION;
if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to broadcast");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsString(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (JS_IsArrayBuffer(ctx,argv[0])) {
buf = JS_GetArrayBuffer(ctx, &data_len, argv[0]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_ThrowTypeError(ctx, "broadcast() only accepts a string or ArrayBuffer");
} }
if (argc < 1) { ENetPacket *packet = enet_packet_create(data_str ? data_str : buf, data_len, ENET_PACKET_FLAG_RELIABLE);
return JS_ThrowTypeError(ctx, "Expected an object to broadcast."); if (data_str) JS_FreeCString(ctx, data_str);
} if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");
// Must be a JavaScript object
if (!JS_IsObject(argv[0])) {
return JS_ThrowTypeError(ctx,
"broadcast() only accepts a plain JS object, not strings/numbers.");
}
// JSON.stringify the object
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
if (JS_IsException(json_data)) {
return JS_ThrowTypeError(ctx,
"Failed to stringify object (circular ref or non-serializable).");
}
size_t data_len;
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
JS_FreeValue(ctx, json_data);
if (!data_str) {
return JS_EXCEPTION; // out of memory
}
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
JS_FreeCString(ctx, data_str);
if (!packet) {
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
}
enet_host_broadcast(host, 0, packet); enet_host_broadcast(host, 0, packet);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
ENetHost *host = JS_GetOpaque(self, enet_host_id);
if (!host) return JS_EXCEPTION;
return JS_NewInt32(js, host->address.port);
}
static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
ENetHost *me = JS_GetOpaque(self, enet_host_id);
if (!me) return JS_EXCEPTION;
uint32_t host = ntohl(me->address.host);
if (host == 0x7F000001) return JS_NewString(js, "localhost");
char ip_str[16];
snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u",
(host >> 24) & 0xFF,
(host >> 16) & 0xFF,
(host >> 8) & 0xFF,
host & 0xFF);
return JS_NewString(js, ip_str);
}
/* Peer-level operations */ /* Peer-level operations */
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_peer_disconnect(peer, 0); enet_peer_disconnect(peer, 0);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
/* Peer send must only accept an object */ /* Peer send must only accept string or ArrayBuffer */
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to send");
const char *data_str = NULL;
size_t data_len = 0;
uint8_t *buf = NULL;
if (JS_IsString(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION;
} else if (JS_IsArrayBuffer(ctx,argv[0])) {
buf = JS_GetArrayBuffer(ctx, &data_len, argv[0]);
if (!buf) return JS_EXCEPTION;
} else {
return JS_ThrowTypeError(ctx, "send() only accepts a string or ArrayBuffer");
} }
if (argc < 1) { ENetPacket *packet = enet_packet_create(data_str ? data_str : buf, data_len, ENET_PACKET_FLAG_RELIABLE);
return JS_ThrowTypeError(ctx, "Expected an object to send."); if (data_str) JS_FreeCString(ctx, data_str);
} if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");
if (!JS_IsObject(argv[0])) {
return JS_ThrowTypeError(ctx,
"peer.send() only accepts a plain JS object, not strings/numbers.");
}
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL); if (enet_peer_send(peer, 0, packet) < 0) return JS_ThrowInternalError(ctx, "Unable to send packet");
if (JS_IsException(json_data)) {
return JS_ThrowTypeError(ctx,
"Failed to stringify object (circular ref or non-serializable).");
}
size_t data_len;
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
JS_FreeValue(ctx, json_data);
if (!data_str) {
return JS_EXCEPTION;
}
// Create packet
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
JS_FreeCString(ctx, data_str);
if (!packet) {
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
}
if (enet_peer_send(peer, 0, packet) < 0) {
return JS_ThrowInternalError(ctx, "enet_peer_send returned error.");
}
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_peer_disconnect_now(peer, 0); enet_peer_disconnect_now(peer, 0);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_peer_disconnect_later(peer, 0); enet_peer_disconnect_later(peer, 0);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_peer_reset(peer); enet_peer_reset(peer);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
enet_peer_ping(peer); enet_peer_ping(peer);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
int interval, acceleration, deceleration; int interval, acceleration, deceleration;
if (argc < 3 || if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2]))
JS_ToInt32(ctx, &interval, argv[0]) || return JS_ThrowTypeError(ctx, "Expected 3 int arguments: interval, acceleration, deceleration");
JS_ToInt32(ctx, &acceleration, argv[1]) ||
JS_ToInt32(ctx, &deceleration, argv[2])) {
return JS_ThrowTypeError(ctx,
"Expected 3 int arguments: interval, acceleration, deceleration");
}
enet_peer_throttle_configure(peer, interval, acceleration, deceleration); enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
int argc, JSValueConst *argv) { {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) { if (!peer) return JS_EXCEPTION;
return JS_EXCEPTION;
}
int timeout_limit, timeout_min, timeout_max; int timeout_limit, timeout_min, timeout_max;
if (argc < 3 || if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2]))
JS_ToInt32(ctx, &timeout_limit, argv[0]) || return JS_ThrowTypeError(ctx, "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
JS_ToInt32(ctx, &timeout_min, argv[1]) ||
JS_ToInt32(ctx, &timeout_max, argv[2])) {
return JS_ThrowTypeError(ctx,
"Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
}
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max); enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
return JS_UNDEFINED; return JS_UNDEFINED;
@@ -415,13 +365,23 @@ static JSClassDef enet_host = {
static JSClassDef enet_peer_class = { static JSClassDef enet_peer_class = {
"ENetPeer", "ENetPeer",
.finalizer = js_enet_peer_finalizer, .finalizer = js_enet_peer_finalizer,
.gc_mark = js_enet_peer_mark
}; };
JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
{
// TODO: implement
const char *hostname = JS_ToCString(js, argv[0]);
JS_FreeCString(js, hostname);
return JS_UNDEFINED;
}
/* Function lists */ /* Function lists */
static const JSCFunctionListEntry js_enet_funcs[] = { static const JSCFunctionListEntry js_enet_funcs[] = {
JS_CFUNC_DEF("initialize", 0, js_enet_initialize), JS_CFUNC_DEF("initialize", 0, js_enet_initialize),
JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize), JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize),
JS_CFUNC_DEF("create_host", 1, js_enet_host_create), JS_CFUNC_DEF("create_host", 1, js_enet_host_create),
JS_CFUNC_DEF("resolve_hostname", 1, js_enet_resolve_hostname),
}; };
static const JSCFunctionListEntry js_enet_host_funcs[] = { static const JSCFunctionListEntry js_enet_host_funcs[] = {
@@ -429,8 +389,118 @@ static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("connect", 2, js_enet_host_connect), JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
JS_CFUNC_DEF("flush", 0, js_enet_host_flush), JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast), JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
}; };
/* Peer getters */
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->roundTripTime);
}
static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
if (peer->incomingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->incomingBandwidth);
}
static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
if (peer->outgoingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->outgoingBandwidth);
}
static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->lastSendTime);
}
static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_EXCEPTION;
return JS_NewInt32(ctx, peer->lastReceiveTime);
}
static JSValue js_enet_peer_get_mtu(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->mtu);
}
static JSValue js_enet_peer_get_outgoing_data_total(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->outgoingDataTotal);
}
static JSValue js_enet_peer_get_incoming_data_total(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->incomingDataTotal);
}
static JSValue js_enet_peer_get_rtt_variance(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->roundTripTimeVariance);
}
static JSValue js_enet_peer_get_packet_loss(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->packetLoss);
}
static JSValue js_enet_peer_get_state(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewInt32(ctx, -1);
return JS_NewInt32(ctx, peer->state);
}
static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValueConst this_val)
{
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) return JS_NewFloat64(ctx, INFINITY);
return JS_NewInt32(ctx, peer->reliableDataInTransit);
}
static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self)
{
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
return JS_NewUint32(js, peer->address.port);
}
static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self)
{
ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
uint32_t host = ntohl(peer->address.host);
if (host == 0x7F000001) return JS_NewString(js, "localhost");
char ip_str[16];
snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u",
(host >> 24) & 0xFF,
(host >> 16) & 0xFF,
(host >> 8) & 0xFF,
host & 0xFF);
return JS_NewString(js, ip_str);
}
static const JSCFunctionListEntry js_enet_peer_funcs[] = { static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("send", 1, js_enet_peer_send), JS_CFUNC_DEF("send", 1, js_enet_peer_send),
JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect), JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect),
@@ -438,30 +508,41 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later), JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later),
JS_CFUNC_DEF("reset", 0, js_enet_peer_reset), JS_CFUNC_DEF("reset", 0, js_enet_peer_reset),
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping), JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
JS_CFUNC_DEF("throttle_configure",3, js_enet_peer_throttle_configure), JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout), JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
}; };
/* Module entry point */ /* Module entry point */
static int js_enet_init(JSContext *ctx, JSModuleDef *m); static int js_enet_init(JSContext *ctx, JSModuleDef *m);
/* This function returns the default export object */ JSValue js_enet_use(JSContext *ctx)
JSValue js_enet_use(JSContext *ctx) { {
// Register ENetHost class
JS_NewClassID(&enet_host_id); JS_NewClassID(&enet_host_id);
JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host); JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host);
JSValue host_proto = JS_NewObject(ctx); JSValue host_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs)); JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
JS_SetClassProto(ctx, enet_host_id, host_proto); JS_SetClassProto(ctx, enet_host_id, host_proto);
// Register ENetPeer class
JS_NewClassID(&enet_peer_class_id); JS_NewClassID(&enet_peer_class_id);
JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class); JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class);
JSValue peer_proto = JS_NewObject(ctx); JSValue peer_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs)); JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto); JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);
// Optional: store references in a "prosperon.c_types" for your environment
JSValue global = JS_GetGlobalObject(ctx); JSValue global = JS_GetGlobalObject(ctx);
JSValue prosp = JS_GetPropertyStr(ctx, global, "prosperon"); JSValue prosp = JS_GetPropertyStr(ctx, global, "prosperon");
JSValue c_types = JS_GetPropertyStr(ctx, prosp, "c_types"); JSValue c_types = JS_GetPropertyStr(ctx, prosp, "c_types");
@@ -473,13 +554,13 @@ JSValue js_enet_use(JSContext *ctx) {
JS_FreeValue(ctx, prosp); JS_FreeValue(ctx, prosp);
JS_FreeValue(ctx, global); JS_FreeValue(ctx, global);
// Create the default export object with top-level ENet functions
JSValue export_obj = JS_NewObject(ctx); JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs)); JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs));
return export_obj; return export_obj;
} }
static int js_enet_init(JSContext *ctx, JSModuleDef *m) { static int js_enet_init(JSContext *ctx, JSModuleDef *m)
{
return JS_SetModuleExport(ctx, m, "default", js_enet_use(ctx)); return JS_SetModuleExport(ctx, m, "default", js_enet_use(ctx));
} }
@@ -489,12 +570,10 @@ static int js_enet_init(JSContext *ctx, JSModuleDef *m) {
#define JS_INIT_MODULE js_init_module_enet #define JS_INIT_MODULE js_init_module_enet
#endif #endif
/* Module definition */ JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { {
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_enet_init); JSModuleDef *m = JS_NewCModule(ctx, module_name, js_enet_init);
if (!m) { if (!m) return NULL;
return NULL;
}
JS_AddModuleExport(ctx, m, "default"); JS_AddModuleExport(ctx, m, "default");
return m; return m;
} }

269
source/qjs_http.c Normal file
View File

@@ -0,0 +1,269 @@
#include "quickjs.h"
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
// -----------------------------------------------------------------------------
// HTTP request structure
// -----------------------------------------------------------------------------
typedef struct {
char url[512]; // URL for the request
JSContext *ctx; // JS context for callbacks
JSValue callback; // Completion callback (optional)
JSValue on_data; // Streaming data callback (optional)
char *response; // Buffer for non-streaming mode
size_t size; // Size of response buffer
CURL *curl; // CURL easy handle
int done; // Request completion flag
int curl_result; // CURL result code
} HttpRequest;
// -----------------------------------------------------------------------------
// Global data
// -----------------------------------------------------------------------------
static CURLM *g_curl_multi = NULL; // CURL multi-handle for async I/O
// -----------------------------------------------------------------------------
// Libcurl write callback for streaming or buffering
// -----------------------------------------------------------------------------
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
HttpRequest *req = (HttpRequest *)userp;
// Streaming mode: call onData if provided
if (JS_IsFunction(req->ctx, req->on_data)) {
JSValue chunk = JS_NewStringLen(req->ctx, contents, realsize);
JSValue ret = JS_Call(req->ctx, req->on_data, JS_UNDEFINED, 1, &chunk);
JS_FreeValue(req->ctx, chunk);
JS_FreeValue(req->ctx, ret); // Ignore return value
return realsize;
}
// Non-streaming mode: buffer the response
char *ptr = realloc(req->response, req->size + realsize + 1);
if (!ptr) {
return 0; // Out of memory, tell CURL to abort
}
req->response = ptr;
memcpy(&(req->response[req->size]), contents, realsize);
req->size += realsize;
req->response[req->size] = '\0'; // Null-terminate
return realsize;
}
// -----------------------------------------------------------------------------
// JS function: http.fetch(url, options)
// - Enqueues an async HTTP request with optional streaming
// -----------------------------------------------------------------------------
static JSValue js_http_fetch(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (argc < 2 || !JS_IsString(argv[0])) {
return JS_ThrowTypeError(ctx, "fetch expects a URL string and an options object or callback");
}
// Get URL
const char *url = JS_ToCString(ctx, argv[0]);
if (!url) {
return JS_ThrowTypeError(ctx, "Invalid URL");
}
// Allocate request object
HttpRequest *req = calloc(1, sizeof(*req));
if (!req) {
JS_FreeCString(ctx, url);
return JS_ThrowInternalError(ctx, "Failed to allocate memory");
}
strncpy(req->url, url, sizeof(req->url) - 1);
req->ctx = ctx;
req->callback = JS_NULL;
req->on_data = JS_NULL;
// Parse second argument: callback or options object
if (JS_IsFunction(ctx, argv[1])) {
req->callback = JS_DupValue(ctx, argv[1]);
} else if (JS_IsObject(argv[1])) {
JSValue callback = JS_GetPropertyStr(ctx, argv[1], "callback");
JSValue on_data = JS_GetPropertyStr(ctx, argv[1], "on_data");
if (JS_IsFunction(ctx, callback)) {
req->callback = JS_DupValue(ctx, callback);
}
if (JS_IsFunction(ctx, on_data)) {
req->on_data = JS_DupValue(ctx, on_data);
}
JS_FreeValue(ctx, callback);
JS_FreeValue(ctx, on_data);
} else {
JS_FreeCString(ctx, url);
free(req);
return JS_ThrowTypeError(ctx, "Second argument must be a callback or options object");
}
JS_FreeCString(ctx, url);
// Initialize CURL easy handle
req->curl = curl_easy_init();
if (!req->curl) {
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "Failed to create CURL handle");
}
// Set CURL options
curl_easy_setopt(req->curl, CURLOPT_URL, req->url);
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, req);
curl_easy_setopt(req->curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "prosperon");
curl_easy_setopt(req->curl, CURLOPT_PRIVATE, req);
// Add to multi-handle
if (!g_curl_multi) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "CURL multi-handle not initialized");
}
CURLMcode mc = curl_multi_add_handle(g_curl_multi, req->curl);
if (mc != CURLM_OK) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "curl_multi_add_handle failed: %s", curl_multi_strerror(mc));
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// JS function: http.poll()
// - Checks for I/O and completed requests, invoking callbacks as needed
// -----------------------------------------------------------------------------
static JSValue js_http_poll(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (!g_curl_multi) {
return JS_UNDEFINED;
}
// Perform pending transfers
int still_running = 0;
CURLMcode mc = curl_multi_perform(g_curl_multi, &still_running);
if (mc != CURLM_OK) {
return JS_ThrowInternalError(ctx, "curl_multi_perform failed: %s", curl_multi_strerror(mc));
}
// Check for completed requests
CURLMsg *msg;
int msgq = 0;
while ((msg = curl_multi_info_read(g_curl_multi, &msgq))) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
HttpRequest *req = NULL;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char **)&req);
char *ct = NULL;
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
// Remove from multi-handle
curl_multi_remove_handle(g_curl_multi, easy);
// Mark as done
req->curl_result = msg->data.result;
req->done = 1;
// Call completion callback if provided
if (JS_IsFunction(req->ctx, req->callback)) {
JSValue arg = JS_NewObject(req->ctx);
if (req->curl_result == CURLE_OK) {
JS_SetPropertyStr(req->ctx, arg, "data",
JS_NewArrayBufferCopy(req->ctx, req->response, req->size));
JS_SetPropertyStr(req->ctx, arg, "error", JS_UNDEFINED);
if (ct) JS_SetPropertyStr(req->ctx, arg, "type", JS_NewString(req->ctx, ct));
} else {
JS_DefinePropertyValueStr(req->ctx, arg, "data", JS_NULL, JS_PROP_C_W_E);
const char *err_str = curl_easy_strerror(req->curl_result);
JS_DefinePropertyValueStr(req->ctx, arg, "error",
JS_NewString(req->ctx, err_str ? err_str : "Unknown error"),
JS_PROP_C_W_E);
}
JSValue ret = JS_Call(req->ctx, req->callback, JS_UNDEFINED, 1, &arg);
JS_FreeValue(req->ctx, arg);
JS_FreeValue(req->ctx, ret);
}
// Cleanup
JS_FreeValue(req->ctx, req->callback);
JS_FreeValue(req->ctx, req->on_data);
curl_easy_cleanup(req->curl);
free(req->response);
free(req);
}
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// Module initialization
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_http_funcs[] = {
JS_CFUNC_DEF("fetch", 2, js_http_fetch),
JS_CFUNC_DEF("poll", 0, js_http_poll),
};
JSValue js_http_use(JSContext *ctx)
{
// Initialize CURL globally (once per process)
static int s_curl_init_done = 0;
if (!s_curl_init_done) {
s_curl_init_done = 1;
if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL");
}
}
// Initialize multi-handle (once per module)
if (!g_curl_multi) {
g_curl_multi = curl_multi_init();
if (!g_curl_multi) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL multi-handle");
}
}
// Export fetch and poll functions
JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_http_funcs,
sizeof(js_http_funcs) / sizeof(JSCFunctionListEntry));
return export_obj;
}
static int js_http_init(JSContext *ctx, JSModuleDef *m)
{
JS_SetModuleExport(ctx, m, "default", js_http_use(ctx));
return 0;
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_http
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
{
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_http_init);
if (!m) {
return NULL;
}
JS_AddModuleExport(ctx, m, "default");
return m;
}

8
source/qjs_http.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_HTTP_H
#define QJS_HTTP_H
#include "quickjs.h"
JSValue js_http_use(JSContext*);
#endif

View File

@@ -278,7 +278,7 @@ JSC_SSCALL(imgui_textbox,
ret = JS_DupValue(js,argv[1]); ret = JS_DupValue(js,argv[1]);
) )
JSC_SCALL(imgui_text, ImGui::Text(str) ) JSC_SCALL(imgui_text, ImGui::Text("%s", str) )
JSC_SCALL(imgui_button, JSC_SCALL(imgui_button,
if (ImGui::Button(str)) if (ImGui::Button(str))

View File

@@ -1,4 +1,13 @@
#include <tracy/TracyC.h> #ifdef TRACY_ENABLE
#include <tracy/TracyC.h>
#else
/* Provide harmless stubs when Tracy is not in use so the rest of the code
can still call the hooks unconditionally. */
#define TracyCAllocN(ptr, size, name) ((void)0)
#define TracyCFreeN(ptr, name) ((void)0)
#define TracyCFiberEnter(name) ((void)0)
#define TracyCFiberLeave(name) ((void)0)
#endif
#define MIST_CFUNC_DEF(name, length, func1, props) { name, props, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_generic, { .generic = func1 } } } } #define MIST_CFUNC_DEF(name, length, func1, props) { name, props, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_generic, { .generic = func1 } } } }

View File

@@ -7,8 +7,9 @@
typedef struct NotaEncodeContext { typedef struct NotaEncodeContext {
JSContext *ctx; JSContext *ctx;
JSValue visitedStack; JSValue visitedStack;
NotaBuffer nb; // use the dynamic NotaBuffer NotaBuffer nb;
int cycle; int cycle;
JSValue replacer;
} NotaEncodeContext; } NotaEncodeContext;
static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val) static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val)
@@ -28,12 +29,11 @@ static void nota_stack_pop(NotaEncodeContext *enc)
static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val) static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
{ {
JSContext *ctx = enc->ctx; JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack); int len = JS_ArrayLength(ctx, enc->visitedStack);
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i); JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
if (JS_IsObject(elem) && JS_IsObject(val)) { if (JS_IsObject(elem) && JS_IsObject(val)) {
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) { if (JS_StrictEq(ctx, elem, val)) {
JS_FreeValue(ctx, elem); JS_FreeValue(ctx, elem);
return 1; return 1;
} }
@@ -43,10 +43,19 @@ static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
return 0; return 0;
} }
JSValue number; static JSValue apply_replacer(NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
if (JS_IsUndefined(enc->replacer)) return JS_DupValue(enc->ctx, val);
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
{ JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
int type = nota_type(nota); int type = nota_type(nota);
JSValue ret2; JSValue ret2;
long long n; long long n;
@@ -70,7 +79,7 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
nota = nota_read_array(&n, nota); nota = nota_read_array(&n, nota);
*tmp = JS_NewArray(js); *tmp = JS_NewArray(js);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
nota = js_do_nota_decode(js, &ret2, nota); nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver);
JS_SetPropertyInt64(js, *tmp, i, ret2); JS_SetPropertyInt64(js, *tmp, i, ret2);
} }
break; break;
@@ -79,171 +88,212 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
*tmp = JS_NewObject(js); *tmp = JS_NewObject(js);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
nota = nota_read_text(&str, nota); nota = nota_read_text(&str, nota);
nota = js_do_nota_decode(js, &ret2, nota); JSValue prop_key = JS_NewString(js, str);
nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver);
JS_SetPropertyStr(js, *tmp, str, ret2); JS_SetPropertyStr(js, *tmp, str, ret2);
JS_FreeValue(js, prop_key);
free(str); free(str);
} }
break; break;
case NOTA_INT: case NOTA_INT:
nota = nota_read_int(&n, nota); nota = nota_read_int(&n, nota);
*tmp = JS_NewInt64(js,n); *tmp = JS_NewInt64(js, n);
break; break;
case NOTA_SYM: case NOTA_SYM:
nota = nota_read_sym(&b, nota); nota = nota_read_sym(&b, nota);
switch(b) { switch(b) {
case NOTA_NULL: case NOTA_NULL: *tmp = JS_UNDEFINED; break;
*tmp = JS_UNDEFINED; case NOTA_FALSE: *tmp = JS_NewBool(js, 0); break;
break; case NOTA_TRUE: *tmp = JS_NewBool(js, 1); break;
case NOTA_FALSE:
*tmp = JS_NewBool(js,0);
break;
case NOTA_TRUE:
*tmp = JS_NewBool(js,1);
break;
} }
break; break;
default: default:
case NOTA_FLOAT: case NOTA_FLOAT:
nota = nota_read_float(&d, nota); nota = nota_read_float(&d, nota);
*tmp = JS_NewFloat64(js,d); *tmp = JS_NewFloat64(js, d);
break; break;
} }
if (!JS_IsUndefined(reviver)) {
JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) };
JSValue revived = JS_Call(js, reviver, holder, 2, args);
JS_FreeValue(js, args[0]);
JS_FreeValue(js, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(js, *tmp);
*tmp = revived;
} else {
JS_FreeValue(js, revived);
}
}
return nota; return nota;
} }
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val); static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
static void encode_object_properties(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx; JSContext *ctx = enc->ctx;
JSValue replaced = apply_replacer(enc, holder, key, val);
JSPropertyEnum *ptab; int tag = JS_VALUE_GET_TAG(replaced);
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
nota_write_sym(&enc->nb, NOTA_NULL);
return;
}
nota_write_record(&enc->nb, plen);
for (uint32_t i = 0; i < plen; i++) {
// property name
const char *propName = JS_AtomToCString(ctx, ptab[i].atom);
nota_write_text(&enc->nb, propName);
JS_FreeCString(ctx, propName);
// property value
JSValue propVal = JS_GetProperty(ctx, val, ptab[i].atom);
nota_encode_value(enc, propVal);
JS_FreeValue(ctx, propVal);
// free the atom
JS_FreeAtom(ctx, ptab[i].atom);
}
js_free(ctx, ptab);
}
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int tag = JS_VALUE_GET_TAG(val);
switch (tag) { switch (tag) {
case JS_TAG_INT: { case JS_TAG_INT: {
double d; double d;
JS_ToFloat64(ctx, &d, val); JS_ToFloat64(ctx, &d, replaced);
nota_write_number(&enc->nb, d); nota_write_number(&enc->nb, d);
return; break;
} }
case JS_TAG_BIG_INT: case JS_TAG_BIG_INT:
case JS_TAG_FLOAT64: case JS_TAG_FLOAT64: {
case JS_TAG_BIG_DECIMAL: const char *str = JS_ToCString(ctx, replaced);
case JS_TAG_BIG_FLOAT: {
const char *str = JS_ToCString(ctx, val);
nota_write_number_str(&enc->nb, str); nota_write_number_str(&enc->nb, str);
JS_FreeCString(ctx, str); JS_FreeCString(ctx, str);
return; break;
} }
case JS_TAG_STRING: { case JS_TAG_STRING: {
const char *str = JS_ToCString(ctx, val); const char *str = JS_ToCString(ctx, replaced);
nota_write_text(&enc->nb, str); nota_write_text(&enc->nb, str);
JS_FreeCString(ctx, str); JS_FreeCString(ctx, str);
return; break;
} }
case JS_TAG_BOOL:
case JS_TAG_BOOL: { if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE);
if (JS_VALUE_GET_BOOL(val)) else nota_write_sym(&enc->nb, NOTA_FALSE);
nota_write_sym(&enc->nb, NOTA_TRUE); break;
else
nota_write_sym(&enc->nb, NOTA_FALSE);
return;
}
case JS_TAG_NULL: case JS_TAG_NULL:
case JS_TAG_UNDEFINED: case JS_TAG_UNDEFINED:
nota_write_sym(&enc->nb, NOTA_NULL); nota_write_sym(&enc->nb, NOTA_NULL);
return; break;
case JS_TAG_OBJECT: { case JS_TAG_OBJECT: {
size_t bufLen; if (JS_IsArrayBuffer(ctx, replaced)) {
void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val); size_t buf_len;
if (bufData) { void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced);
/* Write as a blob of bits (bufLen * 8). */ nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data);
nota_write_blob(&enc->nb, (unsigned long long)bufLen * 8, (const char*)bufData); break;
return;
} }
if (JS_IsArray(ctx, val)) { if (JS_IsArray(ctx, replaced)) {
if (nota_stack_has(enc, val)) { if (nota_stack_has(enc, replaced)) {
enc->cycle = 1; enc->cycle = 1;
return; // bail out break;
} }
nota_stack_push(enc, val); nota_stack_push(enc, replaced);
int arr_len = JS_ArrayLength(ctx, replaced);
int arrLen = JS_ArrayLength(ctx, val); nota_write_array(&enc->nb, arr_len);
nota_write_array(&enc->nb, arrLen); for (int i = 0; i < arr_len; i++) {
for (int i = 0; i < arrLen; i++) { JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSValue elemVal = JS_GetPropertyUint32(ctx, val, i); JSValue elem_key = JS_NewInt32(ctx, i);
nota_encode_value(enc, elemVal); nota_encode_value(enc, elem_val, replaced, elem_key);
JS_FreeValue(ctx, elemVal); JS_FreeValue(ctx, elem_val);
JS_FreeValue(ctx, elem_key);
} }
nota_stack_pop(enc); nota_stack_pop(enc);
return; break;
} }
if (nota_stack_has(enc, val)) { if (nota_stack_has(enc, replaced)) {
enc->cycle = 1; enc->cycle = 1;
return; // bail out break;
} }
nota_stack_push(enc, replaced);
nota_stack_push(enc, val); JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
encode_object_properties(enc, val); if (JS_IsFunction(ctx, to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
nota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else {
nota_write_sym(&enc->nb, NOTA_NULL);
}
nota_stack_pop(enc); nota_stack_pop(enc);
return; break;
}
JS_FreeValue(ctx, to_json);
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
nota_write_sym(&enc->nb, NOTA_NULL);
nota_stack_pop(enc);
break;
} }
uint32_t non_function_count = 0;
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) non_function_count++;
JS_FreeValue(ctx, prop_val);
}
nota_write_record(&enc->nb, non_function_count);
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) {
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
nota_write_text(&enc->nb, prop_name);
nota_encode_value(enc, prop_val, replaced, prop_key);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_key);
}
JS_FreeValue(ctx, prop_val);
JS_FreeAtom(ctx, ptab[i].atom);
}
js_free(ctx, ptab);
nota_stack_pop(enc);
break;
}
default: default:
nota_write_sym(&enc->nb, NOTA_NULL); nota_write_sym(&enc->nb, NOTA_NULL);
return; break;
} }
JS_FreeValue(ctx, replaced);
} }
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) void *value2nota(JSContext *ctx, JSValue v) {
{ NotaEncodeContext enc_s, *enc = &enc_s;
if (argc < 1) enc->ctx = ctx;
return JS_ThrowTypeError(ctx, "nota.encode requires 1 argument"); enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = JS_UNDEFINED;
nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, v, JS_UNDEFINED, JS_NewString(ctx, ""));
if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack);
nota_buffer_free(&enc->nb);
return NULL;
}
JS_FreeValue(ctx, enc->visitedStack);
void *data_ptr = enc->nb.data;
enc->nb.data = NULL;
nota_buffer_free(&enc->nb);
return data_ptr;
}
JSValue nota2value(JSContext *js, char *nota) {
if (!nota) return JS_UNDEFINED;
JSValue ret;
JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_UNDEFINED);
JS_FreeValue(js, holder);
return ret;
}
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument");
NotaEncodeContext enc_s, *enc = &enc_s; NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx; enc->ctx = ctx;
enc->visitedStack = JS_NewArray(ctx); // empty array initially enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0; enc->cycle = 0;
enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED;
nota_buffer_init(&enc->nb, 128); nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, argv[0], JS_UNDEFINED, JS_NewString(ctx, ""));
nota_encode_value(enc, argv[0]);
if (enc->cycle) { if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack); JS_FreeValue(ctx, enc->visitedStack);
@@ -252,26 +302,26 @@ static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, J
} }
JS_FreeValue(ctx, enc->visitedStack); JS_FreeValue(ctx, enc->visitedStack);
size_t total_len = enc->nb.size;
size_t totalLen = enc->nb.size; // how many bytes used void *data_ptr = enc->nb.data;
void* dataPtr = enc->nb.data; // pointer to the raw data JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)data_ptr, total_len);
JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)dataPtr, totalLen);
nota_buffer_free(&enc->nb); nota_buffer_free(&enc->nb);
return ret; return ret;
} }
JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv) static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
{
if (argc < 1) return JS_UNDEFINED; if (argc < 1) return JS_UNDEFINED;
size_t len; size_t len;
unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]);
if (!nota) return JS_UNDEFINED; if (!nota) return JS_UNDEFINED;
JSValue reviver = (argc > 1 && JS_IsFunction(js, argv[1])) ? argv[1] : JS_UNDEFINED;
JSValue ret; JSValue ret;
js_do_nota_decode(js, &ret, (char*)nota); JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver);
JS_FreeValue(js, holder);
return ret; return ret;
} }
@@ -281,18 +331,13 @@ static const JSCFunctionListEntry js_nota_funcs[] = {
}; };
static int js_nota_init(JSContext *ctx, JSModuleDef *m) { static int js_nota_init(JSContext *ctx, JSModuleDef *m) {
JS_SetModuleExportList(ctx, m, js_nota_funcs, JS_SetModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
return 0; return 0;
} }
JSValue js_nota_use(JSContext *js) JSValue js_nota_use(JSContext *js) {
{
JSValue export = JS_NewObject(js); JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
js_nota_funcs,
sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
number = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "Number");
return export; return export;
} }
@@ -305,7 +350,6 @@ JSValue js_nota_use(JSContext *js)
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init); JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init);
if (!m) return NULL; if (!m) return NULL;
JS_AddModuleExportList(ctx, m, js_nota_funcs, JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
return m; return m;
} }

View File

@@ -5,4 +5,7 @@
JSValue js_nota_use(JSContext*); JSValue js_nota_use(JSContext*);
void *value2nota(JSContext*, JSValue);
JSValue nota2value(JSContext*, void*);
#endif #endif

321
source/qjs_qr.c Normal file
View File

@@ -0,0 +1,321 @@
#include "quickjs.h"
#include "quirc.h"
#include "qrencode.h"
#include <math.h> // for sqrt
#include <stdlib.h> // for size_t, etc.
#include <string.h> // for memcpy, strcmp
#include <stdint.h>
#include <errno.h>
// QR encode function
static JSValue js_qr_encode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer as first argument");
}
// Default options
int version = 0; // 0 means auto-select in qrencode
int errc = QR_ECLEVEL_L; // Default to "L"
int casesensitive = 1; // Default to true (qrencode treats 1 as case-sensitive)
int use_byte_mode = 0;
// Handle options object (argv[1])
if (argc > 1 && !JS_IsUndefined(argv[1]) && JS_IsObject(argv[1])) {
JSValue opt = argv[1];
JSValue m = JS_GetPropertyStr(js, opt, "mode");
if (!JS_IsUndefined(m)) {
const char *smode = JS_ToCString(js, m);
if (!smode) { JS_FreeValue(js, m); return JS_EXCEPTION; }
if (!strcasecmp(smode, "byte") || !strcasecmp(smode, "binary"))
use_byte_mode = 1;
else if (!strcasecmp(smode, "text"))
use_byte_mode = 0;
else {
JS_FreeCString(js, smode); JS_FreeValue(js, m);
return JS_ThrowRangeError(js, "mode must be 'byte' or 'text'");
}
JS_FreeCString(js, smode);
JS_FreeValue(js, m);
}
// Version (1-40)
JSValue v = JS_GetPropertyStr(js, opt, "version");
if (!JS_IsUndefined(v)) {
int32_t ver;
if (JS_ToInt32(js, &ver, v) || ver < 0 || ver > 40) {
JS_FreeValue(js, v);
return JS_ThrowRangeError(js, "version must be between 0 and 40");
}
version = ver;
JS_FreeValue(js, v);
}
// Error correction level ("l", "m", "q", "h")
JSValue l = JS_GetPropertyStr(js, opt, "level");
if (!JS_IsUndefined(l)) {
const char *level = JS_ToCString(js, l);
if (!level) {
JS_FreeValue(js, l);
return JS_ThrowTypeError(js, "level must be a string");
}
if (strcasecmp(level, "l") == 0) errc = QR_ECLEVEL_L;
else if (strcasecmp(level, "m") == 0) errc = QR_ECLEVEL_M;
else if (strcasecmp(level, "q") == 0) errc = QR_ECLEVEL_Q;
else if (strcasecmp(level, "h") == 0) errc = QR_ECLEVEL_H;
else {
JS_FreeCString(js, level);
JS_FreeValue(js, l);
return JS_ThrowRangeError(js, "level must be 'l', 'm', 'q', or 'h'");
}
JS_FreeCString(js, level);
JS_FreeValue(js, l);
}
// Case sensitivity (true/false)
JSValue ci = JS_GetPropertyStr(js, opt, "caseinsensitive");
if (!JS_IsUndefined(ci)) {
int bool_val;
if (JS_ToBool(js, ci)) {
bool_val = 0; // qrencode: 0 = case-insensitive
} else {
bool_val = 1; // qrencode: 1 = case-sensitive
}
casesensitive = bool_val;
JS_FreeValue(js, ci);
}
}
const char *data = NULL;
size_t data_len = 0;
int must_free_data = 0;
// Handle string input
if (!use_byte_mode && JS_IsString(argv[0])) {
data = JS_ToCStringLen(js, &data_len, argv[0]);
if (!data) {
return JS_ThrowTypeError(js, "Invalid string input");
}
must_free_data = 1;
} else {
/* treat everything else as raw bytes */
use_byte_mode = 1;
uint8_t *buf = JS_GetArrayBuffer(js, &data_len, argv[0]);
if (!buf) {
return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer");
}
data = (const char *)buf; // Cast to char* for qrencode
}
// Encode using qrencode
QRcode *qrcode;
if (use_byte_mode)
qrcode = QRcode_encodeData(data_len, data, version, errc);
else
qrcode = QRcode_encodeString(data, version, errc, QR_MODE_8, casesensitive);
if (!qrcode) {
if (must_free_data)
JS_FreeCString(js, data);
return JS_ThrowInternalError(js, "Failed to encode QR code: %s", strerror(errno));
}
// qrcode->width is the size of one side
// qrcode->data is width * width bytes
printf("Encoded %d of data into a QR of width %d\n", data_len, qrcode->width);
int width = qrcode->width;
size_t size = (size_t)width * width; // total modules
// Create an array of booleans for the module data
JSValue dataArr = JS_NewArray(js);
for (size_t i = 0; i < size; i++) {
int is_black = (qrcode->data[i] & 1);
JSValue val = JS_NewBool(js, is_black);
JS_SetPropertyUint32(js, dataArr, (uint32_t)i, val);
}
// Build a JS object to hold { width, data }
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, result, "data", dataArr);
// Clean up
QRcode_free(qrcode);
if (must_free_data) {
JS_FreeCString(js, data);
}
return result;
}
static uint8_t rgba_to_gray(uint8_t r, uint8_t g, uint8_t b)
{
return (uint8_t)(( 299 * r + 587 * g + 114 * b + 500) / 1000);
}
// QR decode function (unchanged)
static JSValue js_qr_decode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
size_t data_len;
uint8_t *src;
int w, h, pitch;
src = JS_GetArrayBuffer(js, &data_len, argv[0]);
if (!src)
return JS_ThrowTypeError(js, "decode expects an ArrayBuffer");
JS_ToInt32(js, &w, argv[1]);
JS_ToInt32(js, &h, argv[2]);
JS_ToInt32(js, &pitch, argv[3]);
if (w <= 0 || h <= 0)
return JS_ThrowInternalError(js, "Bad width or height: %dx%d", w, h);
struct quirc *qr = quirc_new();
if (!qr)
return JS_ThrowInternalError(js, "Failed to initialize QR decoder");
if (quirc_resize(qr, w, h) < 0) {
quirc_destroy(qr);
return JS_ThrowInternalError(js, "quirc_resize failed");
}
uint8_t *dst = quirc_begin(qr, NULL, NULL);
printf("decoding image size %dx%d, pitch %d and size %d\n", w, h, pitch, data_len);
for (int y = 0; y < h; ++y) {
uint8_t *row = src + y * pitch;
uint8_t *out = dst + y * w;
for (int x = 0; x < w; ++x) {
uint8_t a = row[x*4 + 3]; /* alpha */
if (a < 128) { /* mostly transparent */
out[x] = 255;
} else {
uint8_t r = row[x*4+0];
uint8_t g = row[x*4+1];
uint8_t b = row[x*4+2];
out[x] = rgba_to_gray(r, g, b);
}
}
}
quirc_end(qr);
int count = quirc_count(qr);
JSValue result = JS_NewArray(js);
for (int i = 0; i < count; i++) {
struct quirc_code code;
struct quirc_data qdata;
quirc_extract(qr, i, &code);
int err = quirc_decode(&code, &qdata);
if (err == QUIRC_SUCCESS) {
JSValue item = JS_NewArrayBufferCopy(js, qdata.payload, qdata.payload_len);
JS_SetPropertyUint32(js, result, i, item);
} else {
printf("QR error: %s\n", quirc_strerror(err));
}
}
quirc_destroy(qr);
return result;
}
static JSValue js_qr_rgba(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "qr.rgba(mods, ...) needs a modules object");
JSValue mods = argv[0];
uint32_t on = 0xFF000000u; /* black opaque */
uint32_t off = 0xFFFFFFFFu; /* white opaque */
if (argc > 1) JS_ToUint32(js, &on, argv[1]);
if (argc > 2) JS_ToUint32(js, &off, argv[2]);
JSValue js_w = JS_GetPropertyStr(js, mods, "width");
if (JS_IsUndefined(js_w)) {
JS_FreeValue(js, js_w);
return JS_ThrowTypeError(js, "mods.width missing");
}
int32_t m = 0;
JS_ToInt32(js, &m, js_w);
JS_FreeValue(js, js_w);
if (m <= 0)
return JS_ThrowRangeError(js, "mods.width must be > 0");
JSValue js_bits = JS_GetPropertyStr(js, mods, "data");
size_t len = JS_ArrayLength(js, js_bits);
if (len < (size_t)m * (size_t)m) {
JS_FreeValue(js, js_bits);
return JS_ThrowRangeError(js, "mods.data too small for width²");
}
const int w = m, h = m, pitch = w * 4;
const size_t bytes = (size_t)h * (size_t)pitch;
uint8_t *tmp = js_malloc(js, bytes);
if (!tmp) { JS_FreeValue(js, js_bits); return JS_EXCEPTION; }
for (int y = 0; y < h; ++y) {
uint32_t *row = (uint32_t *)(tmp + y * pitch);
for (int x = 0; x < w; x++) {
uint32_t idx = y * m + x;
JSValue vbit = JS_GetPropertyUint32(js, js_bits, idx);
int on_bit = JS_ToBool(js, vbit);
JS_FreeValue(js, vbit);
row[x] = on_bit ? on : off;
}
}
JSValue ab = JS_NewArrayBufferCopy(js, tmp, bytes); /* GC owns copy */
js_free(js, tmp);
JS_FreeValue(js, js_bits); /* done with src */
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, w));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, h));
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, pitch));
JS_SetPropertyStr(js, obj, "buffer", ab);
JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
return obj;
}
// Exported functions for the module
static const JSCFunctionListEntry js_qr_funcs[] = {
JS_CFUNC_DEF("encode", 2, js_qr_encode), // Updated to expect 2 args
JS_CFUNC_DEF("decode", 4, js_qr_decode),
JS_CFUNC_DEF("rgba", 3, js_qr_rgba),
};
// Helper to return the default export object
JSValue js_qr_use(JSContext *js) {
JSValue exports = JS_NewObject(js);
JS_SetPropertyFunctionList(js, exports, js_qr_funcs, sizeof(js_qr_funcs) / sizeof(JSCFunctionListEntry));
return exports;
}
// Module init
static int js_qr_init(JSContext *js, JSModuleDef *m) {
JS_SetModuleExport(js, m, "default", js_qr_use(js));
return 0;
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_qr
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *js, const char *module_name) {
JSModuleDef *m = JS_NewCModule(js, module_name, js_qr_init);
if (!m)
return NULL;
JS_AddModuleExport(js, m, "default");
return m;
}

8
source/qjs_qr.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_QR_H
#define QJS_QR_H
#include "quickjs.h"
JSValue js_qr_use(JSContext*);
#endif

506
source/qjs_renderer.c Normal file
View File

@@ -0,0 +1,506 @@
#include "qjs_renderer.h"
#include "quickjs.h"
#include <SDL3/SDL.h>
#include "qjs_macros.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#include "HandmadeMath.h"
QJSCLASS(SDL_Renderer,)
rect transform_rect(rect in, HMM_Mat3 *t)
{
HMM_Vec3 bottom_left = (HMM_Vec3){in.x,in.y,1.0};
HMM_Vec3 transformed_bl = HMM_MulM3V3(*t, bottom_left);
in.x = transformed_bl.x;
in.y = transformed_bl.y;
in.y = in.y - in.h; // should be done for any platform that draws rectangles from top left
return in;
}
HMM_Vec2 transform_point(SDL_Renderer *ren, HMM_Vec2 in, HMM_Mat3 *t)
{
rect logical;
SDL_GetRenderLogicalPresentationRect(ren, &logical);
in.y *= -1;
in.y += logical.h;
in.x -= t->Columns[2].x;
in.y -= t->Columns[2].y;
return in;
}
JSC_CCALL(SDL_Renderer_clear,
SDL_Renderer *renderer = js2SDL_Renderer(js,self);
SDL_RenderClear(renderer);
)
JSC_CCALL(SDL_Renderer_present,
SDL_Renderer *ren = js2SDL_Renderer(js,self);
SDL_RenderPresent(ren);
)
JSC_CCALL(SDL_Renderer_draw_color,
SDL_Renderer *renderer = js2SDL_Renderer(js,self);
colorf color = js2color(js,argv[0]);
SDL_SetRenderDrawColorFloat(renderer, color.r,color.g,color.b,color.a);
)
JSC_CCALL(SDL_Renderer_rect,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (!JS_IsUndefined(argv[1])) {
colorf color = js2color(js,argv[1]);
SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
}
if (JS_IsArray(js,argv[0])) {
int len = js_arrlen(js,argv[0]);
rect rects[len];
for (int i = 0; i < len; i++) {
JSValue val = JS_GetPropertyUint32(js,argv[0],i);
rects[i] = transform_rect(js2rect(js,val), &cam_mat);
JS_FreeValue(js,val);
}
SDL_RenderRects(r,rects,len);
return JS_UNDEFINED;
}
rect rect = js2rect(js,argv[0]);
rect = transform_rect(rect, &cam_mat);
SDL_RenderRect(r, &rect);
)
JSC_CCALL(renderer_load_texture,
SDL_Renderer *r = js2SDL_Renderer(js,self);
SDL_Surface *surf = js2SDL_Surface(js,argv[0]);
if (!surf) return JS_ThrowReferenceError(js, "Surface was not a surface.");
SDL_Texture *tex = SDL_CreateTextureFromSurface(r,surf);
if (!tex) return JS_ThrowReferenceError(js, "Could not create texture from surface: %s", SDL_GetError());
ret = SDL_Texture2js(js,tex);
JS_SetProperty(js,ret,width, number2js(js,tex->w));
JS_SetProperty(js,ret,height, number2js(js,tex->h));
)
JSC_CCALL(SDL_Renderer_fillrect,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (!JS_IsUndefined(argv[1])) {
colorf color = js2color(js,argv[1]);
SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
}
if (JS_IsArray(js,argv[0])) {
int len = js_arrlen(js,argv[0]);
rect rects[len];
for (int i = 0; i < len; i++) {
JSValue val = JS_GetPropertyUint32(js,argv[0],i);
rects[i] = js2rect(js,val);
JS_FreeValue(js,val);
}
if (!SDL_RenderFillRects(r,rects,len))
return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError());
}
rect rect = transform_rect(js2rect(js,argv[0]),&cam_mat);
if (!SDL_RenderFillRect(r, &rect))
return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError());
)
JSC_CCALL(renderer_texture,
SDL_Renderer *renderer = js2SDL_Renderer(js,self);
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
rect dst = transform_rect(js2rect(js,argv[1]), &cam_mat);
if (!JS_IsUndefined(argv[3])) {
colorf color = js2color(js,argv[3]);
SDL_SetTextureColorModFloat(tex, color.r, color.g, color.b);
SDL_SetTextureAlphaModFloat(tex,color.a);
}
if (JS_IsUndefined(argv[2]))
SDL_RenderTexture(renderer,tex,NULL,&dst);
else {
rect src = js2rect(js,argv[2]);
SDL_RenderTextureRotated(renderer, tex, &src, &dst, 0, NULL, SDL_FLIP_NONE);
}
)
JSC_CCALL(renderer_tile,
SDL_Renderer *renderer = js2SDL_Renderer(js,self);
if (!renderer) return JS_ThrowTypeError(js,"self was not a renderer");
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
if (!tex) return JS_ThrowTypeError(js,"first argument was not a texture");
rect dst = js2rect(js,argv[1]);
if (!dst.w) dst.w = tex->w;
if (!dst.h) dst.h = tex->h;
float scale = js2number(js,argv[3]);
if (!scale) scale = 1;
if (JS_IsUndefined(argv[2]))
SDL_RenderTextureTiled(renderer,tex,NULL,scale, &dst);
else {
rect src = js2rect(js,argv[2]);
SDL_RenderTextureTiled(renderer,tex,&src,scale, &dst);
}
)
JSC_CCALL(renderer_slice9,
SDL_Renderer *renderer = js2SDL_Renderer(js,self);
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
lrtb bounds = js2lrtb(js,argv[2]);
rect src, dst;
src = transform_rect(js2rect(js,argv[3]),&cam_mat);
dst = transform_rect(js2rect(js,argv[1]), &cam_mat);
SDL_RenderTexture9Grid(renderer, tex,
JS_IsUndefined(argv[3]) ? NULL : &src,
bounds.l, bounds.r, bounds.t, bounds.b, 0.0,
JS_IsUndefined(argv[1]) ? NULL : &dst);
)
JSC_CCALL(renderer_get_image,
SDL_Renderer *r = js2SDL_Renderer(js,self);
SDL_Surface *surf = NULL;
if (!JS_IsUndefined(argv[0])) {
rect rect = js2rect(js,argv[0]);
surf = SDL_RenderReadPixels(r,&rect);
} else
surf = SDL_RenderReadPixels(r,NULL);
if (!surf) return JS_ThrowReferenceError(js, "could not make surface from renderer");
return SDL_Surface2js(js,surf);
)
JSC_SCALL(renderer_fasttext,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (!JS_IsUndefined(argv[2])) {
colorf color = js2color(js,argv[2]);
SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
}
HMM_Vec2 pos = js2vec2(js,argv[1]);
pos.y += 8;
HMM_Vec2 tpos = HMM_MulM3V3(cam_mat, (HMM_Vec3){pos.x,pos.y,1}).xy;
SDL_RenderDebugText(r, tpos.x, tpos.y, str);
)
JSC_CCALL(renderer_line,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (!JS_IsUndefined(argv[1])) {
colorf color = js2color(js,argv[1]);
SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
}
if (JS_IsArray(js,argv[0])) {
int len = js_arrlen(js,argv[0]);
HMM_Vec2 points[len];
assert(sizeof(HMM_Vec2) == sizeof(SDL_FPoint));
for (int i = 0; i < len; i++) {
JSValue val = JS_GetPropertyUint32(js,argv[0],i);
points[i] = js2vec2(js,val);
JS_FreeValue(js,val);
}
SDL_RenderLines(r,points,len);
}
)
JSC_CCALL(renderer_point,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (!JS_IsUndefined(argv[1])) {
colorf color = js2color(js,argv[1]);
SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
}
if (JS_IsArray(js,argv[0])) {
int len = js_arrlen(js,argv[0]);
HMM_Vec2 points[len];
assert(sizeof(HMM_Vec2) ==sizeof(SDL_FPoint));
for (int i = 0; i < len; i++) {
JSValue val = JS_GetPropertyUint32(js,argv[0],i);
points[i] = js2vec2(js,val);
JS_FreeValue(js,val);
}
SDL_RenderPoints(r, points, len);
return JS_UNDEFINED;
}
HMM_Vec2 point = transform_point(r, js2vec2(js,argv[0]), &cam_mat);
SDL_RenderPoint(r,point.x,point.y);
)
// Function to translate a list of 2D points
void Translate2DPoints(HMM_Vec2 *points, int count, HMM_Vec3 position, HMM_Quat rotation, HMM_Vec3 scale) {
// Precompute the 2D rotation matrix from the quaternion
float xx = rotation.x * rotation.x;
float yy = rotation.y * rotation.y;
float zz = rotation.z * rotation.z;
float xy = rotation.x * rotation.y;
float zw = rotation.z * rotation.w;
// Extract 2D affine rotation and scaling
float m00 = (1.0f - 2.0f * (yy + zz)) * scale.x; // Row 1, Column 1
float m01 = (2.0f * (xy + zw)) * scale.y; // Row 1, Column 2
float m10 = (2.0f * (xy - zw)) * scale.x; // Row 2, Column 1
float m11 = (1.0f - 2.0f * (xx + zz)) * scale.y; // Row 2, Column 2
// Translation components (ignore the z position)
float tx = position.x;
float ty = position.y;
// Transform each point
for (int i = 0; i < count; ++i) {
HMM_Vec2 p = points[i];
points[i].x = m00 * p.x + m01 * p.y + tx;
points[i].y = m10 * p.x + m11 * p.y + ty;
}
}
// Should take a single struct with pos, color, uv, and indices arrays
JSC_CCALL(renderer_geometry,
SDL_Renderer *r = js2SDL_Renderer(js,self);
JSValue pos = JS_GetPropertyStr(js,argv[1], "pos");
JSValue color = JS_GetPropertyStr(js,argv[1], "color");
JSValue uv = JS_GetPropertyStr(js,argv[1], "uv");
JSValue indices = JS_GetPropertyStr(js,argv[1], "indices");
int vertices = js_getnum_str(js, argv[1], "vertices");
int count = js_getnum_str(js, argv[1], "count");
size_t pos_stride, indices_stride, uv_stride, color_stride;
void *posdata = get_gpu_buffer(js,pos, &pos_stride, NULL);
void *idxdata = get_gpu_buffer(js,indices, &indices_stride, NULL);
void *uvdata = get_gpu_buffer(js,uv, &uv_stride, NULL);
void *colordata = get_gpu_buffer(js,color,&color_stride, NULL);
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
HMM_Vec2 *trans_pos = malloc(vertices*sizeof(HMM_Vec2));
memcpy(trans_pos,posdata, sizeof(HMM_Vec2)*vertices);
for (int i = 0; i < vertices; i++)
trans_pos[i] = HMM_MulM3V3(cam_mat, (HMM_Vec3){trans_pos[i].x, trans_pos[i].y, 1}).xy;
if (!SDL_RenderGeometryRaw(r, tex, trans_pos, pos_stride,colordata,color_stride,uvdata, uv_stride, vertices, idxdata, count, indices_stride))
ret = JS_ThrowReferenceError(js, "Error rendering geometry: %s",SDL_GetError());
free(trans_pos);
JS_FreeValue(js,pos);
JS_FreeValue(js,color);
JS_FreeValue(js,uv);
JS_FreeValue(js,indices);
)
JSC_CCALL(renderer_logical_size,
SDL_Renderer *r = js2SDL_Renderer(js,self);
HMM_Vec2 v = js2vec2(js,argv[0]);
SDL_SetRenderLogicalPresentation(r,v.x,v.y,SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
)
JSC_CCALL(renderer_viewport,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (JS_IsUndefined(argv[0]))
SDL_SetRenderViewport(r,NULL);
else {
rect view = js2rect(js,argv[0]);
SDL_SetRenderViewport(r,&view);
}
)
JSC_CCALL(renderer_get_viewport,
SDL_Renderer *r = js2SDL_Renderer(js,self);
SDL_Rect vp;
SDL_GetRenderViewport(r, &vp);
rect re;
re.x = vp.x;
re.y = vp.y;
re.h = vp.h;
re.w = vp.w;
return rect2js(js,re);
)
JSC_CCALL(renderer_clip,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (JS_IsUndefined(argv[0]))
SDL_SetRenderClipRect(r,NULL);
else {
rect view = js2rect(js,argv[0]);
SDL_SetRenderClipRect(r,&view);
}
)
JSC_CCALL(renderer_scale,
SDL_Renderer *r = js2SDL_Renderer(js,self);
HMM_Vec2 v = js2vec2(js,argv[0]);
SDL_SetRenderScale(r, v.x, v.y);
)
JSC_CCALL(renderer_vsync,
SDL_Renderer *r = js2SDL_Renderer(js,self);
SDL_SetRenderVSync(r,js2number(js,argv[0]));
)
// This returns the coordinates inside the
JSC_CCALL(renderer_coords,
SDL_Renderer *r = js2SDL_Renderer(js,self);
HMM_Vec2 pos, coord;
pos = js2vec2(js,argv[0]);
SDL_RenderCoordinatesFromWindow(r,pos.x,pos.y, &coord.x, &coord.y);
return vec22js(js,coord);
)
JSC_CCALL(renderer_camera,
int centered = JS_ToBool(js,argv[1]);
SDL_Renderer *ren = js2SDL_Renderer(js,self);
SDL_Rect vp;
SDL_GetRenderViewport(ren, &vp);
HMM_Mat3 proj;
proj.Columns[0] = (HMM_Vec3){1,0,0};
proj.Columns[1] = (HMM_Vec3){0,-1,0};
if (centered)
proj.Columns[2] = (HMM_Vec3){vp.w/2.0,vp.h/2.0,1};
else
proj.Columns[2] = (HMM_Vec3){0,vp.h,1};
transform *tra = js2transform(js,argv[0]);
HMM_Mat3 view;
view.Columns[0] = (HMM_Vec3){1,0,0};
view.Columns[1] = (HMM_Vec3){0,1,0};
view.Columns[2] = (HMM_Vec3){-tra->pos.x, -tra->pos.y,1};
cam_mat = HMM_MulM3(proj,view);
)
JSC_CCALL(renderer_screen2world,
HMM_Mat3 inv = HMM_InvGeneralM3(cam_mat);
HMM_Vec3 pos = js2vec3(js,argv[0]);
return vec22js(js, HMM_MulM3V3(inv, pos).xy);
)
JSC_CCALL(renderer_target,
SDL_Renderer *r = js2SDL_Renderer(js,self);
if (JS_IsUndefined(argv[0]))
SDL_SetRenderTarget(r, NULL);
else {
SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
SDL_SetRenderTarget(r,tex);
}
)
// Given an array of sprites, make the necessary geometry
// A sprite is expected to have:
// transform: a transform encoding position and rotation. its scale is in pixels - so a scale of 1 means the image will draw only on a single pixel.
// image: a standard prosperon image of a surface, rect, and texture
// color: the color this sprite should be hued by
JSC_CCALL(renderer_make_sprite_mesh,
JSValue sprites = argv[0];
size_t quads = js_arrlen(js,argv[0]);
size_t verts = quads*4;
size_t count = quads*6;
HMM_Vec2 *posdata = malloc(sizeof(*posdata)*verts);
HMM_Vec2 *uvdata = malloc(sizeof(*uvdata)*verts);
HMM_Vec4 *colordata = malloc(sizeof(*colordata)*verts);
for (int i = 0; i < quads; i++) {
JSValue sub = JS_GetPropertyUint32(js,sprites,i);
JSValue jstransform = JS_GetProperty(js,sub,transform);
JSValue jssrc = JS_GetProperty(js,sub,src);
JSValue jscolor = JS_GetProperty(js,sub,color);
HMM_Vec4 color;
rect src;
if (JS_IsUndefined(jssrc))
src = (rect){.x = 0, .y = 0, .w = 1, .h = 1};
else
src = js2rect(js,jssrc);
if (JS_IsUndefined(jscolor))
color = (HMM_Vec4){1,1,1,1};
else
color = js2vec4(js,jscolor);
// Calculate the base index for the current quad
size_t base = i * 4;
// Define the UV coordinates based on the source rectangle
uvdata[base + 0] = (HMM_Vec2){ src.x, src.y + src.h };
uvdata[base + 1] = (HMM_Vec2){ src.x + src.w, src.y + src.h };
uvdata[base + 2] = (HMM_Vec2){ src.x, src.y };
uvdata[base + 3] = (HMM_Vec2){ src.x + src.w, src.y };
colordata[base] = color;
colordata[base+1] = color;
colordata[base+2] = color;
colordata[base+3] = color;
JS_FreeValue(js,jstransform);
JS_FreeValue(js,sub);
JS_FreeValue(js,jscolor);
JS_FreeValue(js,jssrc);
}
ret = JS_NewObject(js);
JS_SetProperty(js, ret, pos, make_gpu_buffer(js, posdata, sizeof(*posdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0));
JS_SetProperty(js, ret, uv, make_gpu_buffer(js, uvdata, sizeof(*uvdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0));
JS_SetProperty(js, ret, color, make_gpu_buffer(js, colordata, sizeof(*colordata) * verts, JS_TYPED_ARRAY_FLOAT32, 4, 0,0));
JS_SetProperty(js, ret, indices, make_quad_indices_buffer(js, quads));
JS_SetProperty(js, ret, vertices, number2js(js, verts));
JS_SetProperty(js, ret, count, number2js(js, count));
)
static const JSCFunctionListEntry js_renderer_funcs[] = {
JS_CFUNC_DEF("clear", 0, js_renderer_clear),
JS_CFUNC_DEF("present", 0, js_renderer_present),
JS_CFUNC_DEF("draw_color", 1, js_renderer_draw_color),
JS_CFUNC_DEF("rect", 2, js_renderer_rect),
JS_CFUNC_DEF("fillrect", 2, js_renderer_fillrect),
JS_CFUNC_DEF("line", 2, js_renderer_line),
JS_CFUNC_DEF("point", 2, js_renderer_point),
JS_CFUNC_DEF("load_texture", 1, js_renderer_load_texture),
JS_CFUNC_DEF("texture", 4, js_renderer_texture),
JS_CFUNC_DEF("slice9", 4, js_renderer_slice9),
JS_CFUNC_DEF("tile", 4, js_renderer_tile),
JS_CFUNC_DEF("get_image", 1, js_renderer_get_image),
JS_CFUNC_DEF("fasttext", 2, js_renderer_fasttext),
JS_CFUNC_DEF("geometry", 2, js_renderer_geometry),
JS_CFUNC_DEF("scale", 1, js_renderer_scale),
JS_CFUNC_DEF("logical_size", 1, js_renderer_logical_size),
JS_CFUNC_DEF("viewport", 1, js_renderer_viewport),
JS_CFUNC_DEF("clip", 1, js_renderer_clip),
JS_CFUNC_DEF("vsync", 1, js_renderer_vsync),
JS_CFUNC_DEF("coords", 1, js_renderer_coords),
JS_CFUNC_DEF("camera", 2, js_renderer_camera),
JS_CFUNC_DEF("get_viewport", 0, js_renderer_get_viewport),
JS_CFUNC_DEF("screen2world", 1, js_renderer_screen2world),
JS_CFUNC_DEF("target", 1, js_renderer_target),
JS_CFUNC_DEF("make_sprite_mesh",2, js_renderer_make_sprite_mesh),
};
JSC_CCALL(mod_create,
SDL_Window *win = js2SDL_Window(js,self);
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetNumberProperty(props, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, 0);
SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, win);
SDL_SetStringProperty(props, SDL_PROP_RENDERER_CREATE_NAME_STRING, str);
SDL_Renderer *r = SDL_CreateRendererWithProperties(props);
SDL_DestroyProperties(props);
if (!r) return JS_ThrowReferenceError(js, "Error creating renderer: %s",SDL_GetError());
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
return SDL_Renderer2js(js,r);
)
static const JSCFunctionListEntry js_mod_funcs[] = {
JS_CFUNC_DEF("create", 1, js_mod_create)
};
JSValue js_renderer_use(JSContext *ctx) {
// Create an object that will hold all the "renderer" methods
JSValue obj = JS_NewObject(ctx);
// Add all the above C functions as properties of that object
JS_SetPropertyFunctionList(ctx, obj,
js_renderer_funcs,
sizeof(js_renderer_funcs)/sizeof(JSCFunctionListEntry));
return obj;
}

8
source/qjs_renderer.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_RENDERER_H
#define QJS_RENDERER_H
#include "quickjs.h"
JSValue js_renderer_use(JSContext *ctx);
#endif /* QJS_RENDERER_H */

View File

@@ -62,14 +62,20 @@ typedef unsigned int voice;
static Soloud *soloud; static Soloud *soloud;
void voice_free(unsigned int *voice)
{
Soloud_stop(soloud, *voice);
free(voice);
}
JSCLASS(Wav, Wav_destroy) JSCLASS(Wav, Wav_destroy)
JSCLASS(voice, free) JSCLASS(voice, voice_free)
JSCLASS(Bus, Bus_destroy) JSCLASS(Bus, Bus_destroy)
static JSValue js_soloud_make(JSContext *js, JSValue self, int argc, JSValue *argv) static JSValue js_soloud_make(JSContext *js, JSValue self, int argc, JSValue *argv)
{ {
soloud = Soloud_create(); soloud = Soloud_create();
Soloud_initEx(soloud, SOLOUD_CLIP_ROUNDOFF, SOLOUD_AUTO, SOLOUD_AUTO, SOLOUD_AUTO, SOLOUD_AUTO); Soloud_initEx(soloud, SOLOUD_CLIP_ROUNDOFF, SOLOUD_NULLDRIVER, 44100, SOLOUD_AUTO, 2);
JSValue obj = JS_NewObject(js); JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "channels", JS_NewFloat64(js, Soloud_getBackendChannels(soloud))); JS_SetPropertyStr(js, obj, "channels", JS_NewFloat64(js, Soloud_getBackendChannels(soloud)));
JS_SetPropertyStr(js, obj, "samplerate", JS_NewFloat64(js, Soloud_getBackendSamplerate(soloud))); JS_SetPropertyStr(js, obj, "samplerate", JS_NewFloat64(js, Soloud_getBackendSamplerate(soloud)));
@@ -87,10 +93,11 @@ static JSValue js_soloud_play(JSContext *js, JSValue self, int argc, JSValue *ar
static JSValue js_soloud_mix(JSContext *js, JSValue self, int argc, JSValue *argv) static JSValue js_soloud_mix(JSContext *js, JSValue self, int argc, JSValue *argv)
{ {
size_t len; int req;
void *data = JS_GetArrayBuffer(js, &len, argv[0]); JS_ToInt32(js, &req, argv[0]);
Soloud_mix(soloud, data, js2number(js,argv[1])); float *buf = malloc(2*req*sizeof(float));
return JS_UNDEFINED; Soloud_mix(soloud, buf, req);
return JS_NewArrayBufferCopy(js, buf, 2*req*sizeof(float));
} }
// Create a voice from a WAV file // Create a voice from a WAV file

45
source/qjs_time.c Normal file
View File

@@ -0,0 +1,45 @@
#include "qjs_time.h"
#include "quickjs.h"
#include <time.h> // For time() calls, localtime, etc.
#include <sys/time.h> // For gettimeofday, if needed
// Example stubs for your time-related calls
static JSValue js_time_now(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
// time_now
struct timeval tv;
gettimeofday(&tv, NULL);
double now = (double)tv.tv_sec + (tv.tv_usec / 1000000.0);
return JS_NewFloat64(ctx, now);
}
static JSValue js_time_computer_dst(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
time_t t = time(NULL);
struct tm *lt = localtime(&t);
int is_dst = (lt ? lt->tm_isdst : -1);
return JS_NewBool(ctx, (is_dst > 0));
}
static JSValue js_time_computer_zone(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
time_t t = time(NULL);
time_t local_t = mktime(localtime(&t));
double diff = difftime(t, local_t); // difference in seconds from local time
return JS_NewFloat64(ctx, diff / 3600.0);
}
static const JSCFunctionListEntry js_time_funcs[] = {
// name, prop flags, #args, etc.
JS_CFUNC_DEF("now", 0, js_time_now),
JS_CFUNC_DEF("computer_dst", 0, js_time_computer_dst),
JS_CFUNC_DEF("computer_zone", 0, js_time_computer_zone),
};
JSValue js_time_use(JSContext *ctx) {
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, obj, js_time_funcs,
sizeof(js_time_funcs)/sizeof(js_time_funcs[0]));
return obj;
}

8
source/qjs_time.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_TIME_H
#define QJS_TIME_H
#include "quickjs.h"
JSValue js_time_use(JSContext *ctx);
#endif /* QJS_TIME_H */

View File

@@ -100,19 +100,6 @@ static JSValue js_tracy_message(JSContext *js, JSValue self, int argc, JSValue *
return JS_UNDEFINED; return JS_UNDEFINED;
} }
static JSValue js_tracy_thread_name(JSContext *js, JSValue self, int argc, JSValue *argv)
{
#ifdef TRACY_ON_DEMAND
if (!TracyCIsConnected)
return JS_UNDEFINED;
#endif
const char *str = JS_ToCString(js, argv[0]);
TracyCSetThreadName(str);
JS_FreeCString(js,str);
return JS_UNDEFINED;
}
static JSValue js_tracy_zone_begin(JSContext *js, JSValue self, int argc, JSValue *argv) static JSValue js_tracy_zone_begin(JSContext *js, JSValue self, int argc, JSValue *argv)
{ {
#ifdef TRACY_ON_DEMAND #ifdef TRACY_ON_DEMAND
@@ -129,11 +116,11 @@ static JSValue js_tracy_zone_begin(JSContext *js, JSValue self, int argc, JSValu
fn_name = "<anonymous>"; fn_name = "<anonymous>";
else else
fn_name = js_fn_name; fn_name = js_fn_name;
TracyCZoneCtx TCTX = ___tracy_emit_zone_begin_alloc(___tracy_alloc_srcloc(js_fn_linenum(js,argv[0]), fn_src, strlen(fn_src), fn_name, strlen(fn_name), (int)fn_src),1); // TracyCZoneCtx TCTX = ___tracy_emit_zone_begin_alloc(___tracy_alloc_srcloc(js_fn_linenum(js,argv[0]), fn_src, strlen(fn_src), fn_name, strlen(fn_name), (int)fn_src),1);
JS_FreeCString(js,js_fn_name); JS_FreeCString(js,js_fn_name);
JS_Call(js, argv[0], JS_UNDEFINED, 0, NULL); JS_Call(js, argv[0], JS_UNDEFINED, 0, NULL);
TracyCZoneEnd(TCTX); // TracyCZoneEnd(TCTX);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
@@ -260,7 +247,7 @@ static JSValue js_tracy_image(JSContext *js, JSValue self, int argc, JSValue *ar
return JS_UNDEFINED; return JS_UNDEFINED;
} }
#elifdef SOKOL_D3D11 #elif defined(SOKOL_D3D11)
#include <d3d11.h> #include <d3d11.h>
static int query_count = 64*1024; static int query_count = 64*1024;
@@ -496,26 +483,57 @@ static JSValue js_tracy_image(JSContext *js, JSValue self, int argc, JSValue *ar
#endif #endif
static TracyCZoneCtx *tracy_stack; // Wrapper struct to keep the array pointer stable
typedef struct {
TracyCZoneCtx *arr; // stb_ds dynamic array
} tracy_stack_t;
// Global TLS ID for the Tracy stack
static SDL_TLSID tracy_stack_id = {0};
// Cleanup function for the wrapper struct
static void tracy_cleanup_stack(void *value)
{
tracy_stack_t *stack = value;
if (stack) {
arrfree(stack->arr);
free(stack);
}
}
// Get or initialize the thread-local Tracy stack
static tracy_stack_t *get_tracy_stack(void)
{
tracy_stack_t *stack = SDL_GetTLS(&tracy_stack_id);
if (!stack) {
stack = malloc(sizeof(tracy_stack_t));
stack->arr = NULL; // stb_ds starts with NULL
arrsetcap(stack->arr, 5); // Initial capacity
SDL_SetTLS(&tracy_stack_id, stack, tracy_cleanup_stack);
}
return stack;
}
void tracy_call_hook(JSContext *js, JSValue fn) void tracy_call_hook(JSContext *js, JSValue fn)
{ {
tracy_stack_t *stack = get_tracy_stack();
js_debug debug; js_debug debug;
js_debug_info(js,fn,&debug); js_debug_info(js, fn, &debug);
uint64_t srcloc; uint64_t srcloc = ___tracy_alloc_srcloc(debug.line, debug.filename, strlen(debug.filename), debug.name, strlen(debug.name), debug.unique);
srcloc = ___tracy_alloc_srcloc(debug.line, debug.source, debug.srclen, debug.name, strlen(debug.name), (int)debug.source); arrput(stack->arr, ___tracy_emit_zone_begin_alloc(srcloc, 1));
arrput(tracy_stack, ___tracy_emit_zone_begin_alloc(srcloc,1)); free_js_debug_info(js, &debug);
} }
void tracy_end_hook(JSContext *js, JSValue fn) void tracy_end_hook(JSContext *js, JSValue fn)
{ {
if (arrlen(tracy_stack) >0) tracy_stack_t *stack = get_tracy_stack();
___tracy_emit_zone_end(arrpop(tracy_stack)); if (arrlen(stack->arr) > 0)
___tracy_emit_zone_end(arrpop(stack->arr));
} }
JSValue js_tracy_level(JSContext *js, JSValue selff, int argc, JSValue *argv) JSValue js_tracy_level(JSContext *js, JSValue self, int argc, JSValue *argv)
{ {
int32_t level; int32_t level;
JS_ToInt32(js,&level, argv[0]); JS_ToInt32(js,&level, argv[0]);
@@ -538,7 +556,6 @@ static const JSCFunctionListEntry js_tracy_funcs[] = {
JS_CFUNC_DEF("gpu_init", 0, js_tracy_gpu_init), JS_CFUNC_DEF("gpu_init", 0, js_tracy_gpu_init),
JS_CFUNC_DEF("gpu_sync", 0, js_tracy_gpu_sync), JS_CFUNC_DEF("gpu_sync", 0, js_tracy_gpu_sync),
JS_CFUNC_DEF("end_frame", 0, js_tracy_frame_mark), JS_CFUNC_DEF("end_frame", 0, js_tracy_frame_mark),
JS_CFUNC_DEF("thread_name", 1, js_tracy_thread_name),
JS_CFUNC_DEF("zone", 1, js_tracy_zone_begin), JS_CFUNC_DEF("zone", 1, js_tracy_zone_begin),
JS_CFUNC_DEF("message", 1, js_tracy_message), JS_CFUNC_DEF("message", 1, js_tracy_message),
JS_CFUNC_DEF("plot", 2, js_tracy_plot), JS_CFUNC_DEF("plot", 2, js_tracy_plot),

369
source/qjs_wota.c Normal file
View File

@@ -0,0 +1,369 @@
#include "quickjs.h"
#include "wota.h"
#include <stdlib.h>
typedef struct WotaEncodeContext {
JSContext *ctx;
JSValue visited_stack;
WotaBuffer wb;
int cycle;
JSValue replacer;
} WotaEncodeContext;
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visited_stack);
JS_SetPropertyInt64(ctx, enc->visited_stack, len, JS_DupValue(ctx, val));
}
static void wota_stack_pop(WotaEncodeContext *enc)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visited_stack);
JS_SetPropertyStr(ctx, enc->visited_stack, "length", JS_NewUint32(ctx, len - 1));
}
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visited_stack);
for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyUint32(ctx, enc->visited_stack, i);
if (JS_IsObject(elem) && JS_IsObject(val))
if (JS_StrictEq(ctx, elem, val)) {
JS_FreeValue(ctx, elem);
return 1;
}
JS_FreeValue(ctx, elem);
}
return 0;
}
static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val)
{
if (JS_IsUndefined(enc->replacer)) return JS_DupValue(enc->ctx, val);
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key);
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder)
{
JSContext *ctx = enc->ctx;
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
return;
}
uint32_t non_function_count = 0;
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) non_function_count++;
JS_FreeValue(ctx, prop_val);
}
wota_write_record(&enc->wb, non_function_count);
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom);
if (!JS_IsFunction(ctx, prop_val)) {
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
wota_write_text(&enc->wb, prop_name);
wota_encode_value(enc, prop_val, val, prop_key);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_key);
}
JS_FreeValue(ctx, prop_val);
JS_FreeAtom(ctx, ptab[i].atom);
}
js_free(ctx, ptab);
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key)
{
JSContext *ctx = enc->ctx;
JSValue replaced;
if (!JS_IsUndefined(enc->replacer) && !JS_IsUndefined(key))
replaced = apply_replacer(enc, holder, key, val);
else
replaced = JS_DupValue(enc->ctx, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT: {
double d;
JS_ToFloat64(ctx, &d, replaced);
wota_write_number(&enc->wb, d);
break;
}
case JS_TAG_FLOAT64:
case JS_TAG_BIG_INT: {
double d;
if (JS_ToFloat64(ctx, &d, replaced) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
wota_write_number(&enc->wb, d);
break;
}
case JS_TAG_STRING: {
const char *str = JS_ToCString(ctx, replaced);
wota_write_text(&enc->wb, str ? str : "");
JS_FreeCString(ctx, str);
break;
}
case JS_TAG_BOOL:
wota_write_sym(&enc->wb, JS_VALUE_GET_BOOL(replaced) ? WOTA_TRUE : WOTA_FALSE);
break;
case JS_TAG_NULL:
case JS_TAG_UNDEFINED:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
case JS_TAG_OBJECT: {
if (JS_IsArrayBuffer(ctx, replaced)) {
size_t buf_len;
void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced);
wota_write_blob(&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
break;
}
if (JS_IsArray(ctx, replaced)) {
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
int arr_len = JS_ArrayLength(ctx, replaced);
wota_write_array(&enc->wb, arr_len);
for (int i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSValue elem_key = JS_NewInt32(ctx, i);
wota_encode_value(enc, elem_val, replaced, elem_key);
JS_FreeValue(ctx, elem_val);
JS_FreeValue(ctx, elem_key);
}
wota_stack_pop(enc);
break;
}
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
if (JS_IsFunction(ctx, to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
wota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else
wota_write_sym(&enc->wb, WOTA_NULL);
wota_stack_pop(enc);
break;
}
JS_FreeValue(ctx, to_json);
encode_object_properties(enc, replaced, holder);
wota_stack_pop(enc);
break;
}
default:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
JS_FreeValue(ctx, replaced);
}
static char *decode_wota_value(JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver)
{
uint64_t first_word = *(uint64_t *)data_ptr;
int type = (int)(first_word & 0xffU);
switch (type) {
case WOTA_INT: {
long long val;
data_ptr = wota_read_int(&val, data_ptr);
*out_val = JS_NewInt64(ctx, val);
break;
}
case WOTA_FLOAT: {
double d;
data_ptr = wota_read_float(&d, data_ptr);
*out_val = JS_NewFloat64(ctx, d);
break;
}
case WOTA_SYM: {
int scode;
data_ptr = wota_read_sym(&scode, data_ptr);
if (scode == WOTA_NULL) *out_val = JS_UNDEFINED;
else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0);
else if (scode == WOTA_TRUE) *out_val = JS_NewBool(ctx, 1);
else *out_val = JS_UNDEFINED;
break;
}
case WOTA_BLOB: {
long long blen;
char *bdata = NULL;
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
*out_val = bdata ? JS_NewArrayBufferCopy(ctx, (uint8_t *)bdata, (size_t)blen) : JS_NewArrayBufferCopy(ctx, NULL, 0);
if (bdata) free(bdata);
break;
}
case WOTA_TEXT: {
char *utf8 = NULL;
data_ptr = wota_read_text(&utf8, data_ptr);
*out_val = JS_NewString(ctx, utf8 ? utf8 : "");
if (utf8) free(utf8);
break;
}
case WOTA_ARR: {
long long c;
data_ptr = wota_read_array(&c, data_ptr);
JSValue arr = JS_NewArray(ctx);
for (long long i = 0; i < c; i++) {
JSValue elem_val = JS_UNDEFINED;
data_ptr = decode_wota_value(ctx, data_ptr, &elem_val, arr, JS_NewInt32(ctx, i), reviver);
JS_SetPropertyUint32(ctx, arr, i, elem_val);
}
*out_val = arr;
break;
}
case WOTA_REC: {
long long c;
data_ptr = wota_read_record(&c, data_ptr);
JSValue obj = JS_NewObject(ctx);
for (long long i = 0; i < c; i++) {
char *tkey = NULL;
data_ptr = wota_read_text(&tkey, data_ptr);
JSValue prop_key = tkey ? JS_NewString(ctx, tkey) : JS_UNDEFINED;
JSValue sub_val = JS_UNDEFINED;
data_ptr = decode_wota_value(ctx, data_ptr, &sub_val, obj, prop_key, reviver);
if (tkey) JS_SetPropertyStr(ctx, obj, tkey, sub_val);
else JS_FreeValue(ctx, sub_val);
JS_FreeValue(ctx, prop_key);
if (tkey) free(tkey);
}
*out_val = obj;
break;
}
default:
data_ptr += 8;
*out_val = JS_UNDEFINED;
break;
}
if (!JS_IsUndefined(reviver)) {
JSValue args[2] = { JS_DupValue(ctx, key), JS_DupValue(ctx, *out_val) };
JSValue revived = JS_Call(ctx, reviver, holder, 2, args);
JS_FreeValue(ctx, args[0]);
JS_FreeValue(ctx, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(ctx, *out_val);
*out_val = revived;
} else
JS_FreeValue(ctx, revived);
}
return data_ptr;
}
void *value2wota(JSContext *ctx, JSValue v, JSValue replacer)
{
WotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_stack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = replacer;
wota_buffer_init(&enc->wb, 16);
wota_encode_value(enc, v, JS_UNDEFINED, JS_UNDEFINED);
if (enc->cycle) {
JS_FreeValue(ctx, enc->visited_stack);
wota_buffer_free(&enc->wb);
return NULL;
}
JS_FreeValue(ctx, enc->visited_stack);
size_t total_bytes = enc->wb.size * sizeof(uint64_t);
void *wota = realloc(enc->wb.data, total_bytes);
return wota;
}
JSValue wota2value(JSContext *ctx, void *wota)
{
JSValue result = JS_UNDEFINED;
JSValue holder = JS_NewObject(ctx);
decode_wota_value(ctx, wota, &result, holder, JS_UNDEFINED, JS_UNDEFINED);
JS_FreeValue(ctx, holder);
return result;
}
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_ThrowTypeError(ctx, "wota.encode requires at least 1 argument");
WotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_stack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED;
wota_buffer_init(&enc->wb, 16);
wota_encode_value(enc, argv[0], JS_UNDEFINED, JS_UNDEFINED);
if (enc->cycle) {
JS_FreeValue(ctx, enc->visited_stack);
wota_buffer_free(&enc->wb);
return JS_ThrowReferenceError(ctx, "Cannot encode cyclic object with wota");
}
JS_FreeValue(ctx, enc->visited_stack);
size_t total_bytes = enc->wb.size * sizeof(uint64_t);
JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t *)enc->wb.data, total_bytes);
wota_buffer_free(&enc->wb);
return ret;
}
static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_UNDEFINED;
size_t len;
uint8_t *buf = JS_GetArrayBuffer(ctx, &len, argv[0]);
if (!buf) return JS_UNDEFINED;
JSValue reviver = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED;
char *data_ptr = (char *)buf;
JSValue result = JS_UNDEFINED;
JSValue holder = JS_NewObject(ctx);
decode_wota_value(ctx, data_ptr, &result, holder, JS_NewString(ctx, ""), reviver);
JS_FreeValue(ctx, holder);
return result;
}
static const JSCFunctionListEntry js_wota_funcs[] = {
JS_CFUNC_DEF("encode", 2, js_wota_encode),
JS_CFUNC_DEF("decode", 2, js_wota_decode),
};
static int js_wota_init(JSContext *ctx, JSModuleDef *m)
{
JS_SetModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return 0;
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_wota
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
{
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_wota_init);
if (!m) return NULL;
JS_AddModuleExportList(ctx, m, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return m;
}
JSValue js_wota_use(JSContext *ctx)
{
JSValue exports = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return exports;
}

12
source/qjs_wota.h Normal file
View File

@@ -0,0 +1,12 @@
#ifndef QJS_WOTA_H
#define QJS_WOTA_H
#include <quickjs.h>
#include "wota.h"
JSValue js_wota_use(JSContext*);
void *value2wota(JSContext*, JSValue, JSValue);
JSValue wota2value(JSContext*, void*);
#endif

View File

@@ -19,8 +19,6 @@ typedef struct viewstate {
extern viewstate globalview; extern viewstate globalview;
void render_init();
void capture_screen(int x, int y, int w, int h, const char *path); void capture_screen(int x, int y, int w, int h, const char *path);
void gif_rec_start(int w, int h, int cpf, int bitdepth); void gif_rec_start(int w, int h, int cpf, int bitdepth);
@@ -57,6 +55,7 @@ struct rect {
}; };
typedef SDL_FRect rect; typedef SDL_FRect rect;
typedef SDL_Rect irect;
float *rgba2floats(float *r, struct rgba c); float *rgba2floats(float *r, struct rgba c);

View File

@@ -1,242 +0,0 @@
#include "script.h"
#include "jsffi.h"
#include "stb_ds.h"
#include <inttypes.h>
#include <limits.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdarg.h>
#include "jsffi.h"
#include <stdlib.h>
#include <assert.h>
#include "quickjs.h"
#if defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(_WIN32)
#include <malloc.h>
#elif defined(__linux__) || defined(__GLIBC__)
#define _GNU_SOURCE
#include <malloc.h>
#endif
#include "physfs.h"
static JSContext *js = NULL;
static JSRuntime *rt = NULL;
JSContext *global_js = NULL;
JSValue on_exception;
#define ENGINE "scripts/core/engine.js"
#define JS_EVAL_FLAGS JS_EVAL_FLAG_STRICT
#ifdef TRACY_ENABLE
#include <tracy/TracyC.h>
#if defined(__APPLE__)
#define MALLOC_OVERHEAD 0
#else
#define MALLOC_OVERHEAD 8
#endif
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* default memory allocation functions with memory limitation */
static size_t js_tracy_malloc_usable_size(const void *ptr)
{
#if defined(__APPLE__)
return malloc_size(ptr);
#elif defined(_WIN32)
return _msize((void *)ptr);
#elif defined(EMSCRIPTEN)
return 0;
#elif defined(__linux__) || defined(__GLIBC__)
return malloc_usable_size((void *)ptr);
#else
/* change this to `return 0;` if compilation fails */
return malloc_usable_size((void *)ptr);
#endif
}
static void *js_tracy_malloc(JSMallocState *s, size_t size)
{
void *ptr;
/* Do not allocate zero bytes: behavior is platform dependent */
assert(size != 0);
if (unlikely(s->malloc_size + size > s->malloc_limit))
return NULL;
ptr = malloc(size);
if (!ptr)
return NULL;
s->malloc_count++;
s->malloc_size += js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
TracyCAllocN(ptr,js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs");
return ptr;
}
static void js_tracy_free(JSMallocState *s, void *ptr)
{
if (!ptr)
return;
s->malloc_count--;
s->malloc_size -= js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
TracyCFreeN(ptr, "quickjs");
free(ptr);
}
static void *js_tracy_realloc(JSMallocState *s, void *ptr, size_t size)
{
size_t old_size;
if (!ptr) {
if (size == 0)
return NULL;
return js_tracy_malloc(s, size);
}
old_size = js_tracy_malloc_usable_size(ptr);
if (size == 0) {
s->malloc_count--;
s->malloc_size -= old_size + MALLOC_OVERHEAD;
TracyCFreeN(ptr, "quickjs");
free(ptr);
return NULL;
}
if (s->malloc_size + size - old_size > s->malloc_limit)
return NULL;
TracyCFreeN(ptr, "quickjs");
ptr = realloc(ptr, size);
if (!ptr)
return NULL;
s->malloc_size += js_tracy_malloc_usable_size(ptr) - old_size;
TracyCAllocN(ptr,js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs");
return ptr;
}
static const JSMallocFunctions tracy_malloc_funcs = {
js_tracy_malloc,
js_tracy_free,
js_tracy_realloc,
js_tracy_malloc_usable_size
};
#endif
void script_startup(int argc, char **argv) {
#ifdef TRACY_ENABLE
rt = JS_NewRuntime2(&tracy_malloc_funcs, NULL);
#else
rt = JS_NewRuntime();
#endif
js = JS_NewContextRaw(rt);
JS_AddIntrinsicBaseObjects(js);
JS_AddIntrinsicEval(js);
JS_AddIntrinsicRegExp(js);
JS_AddIntrinsicJSON(js);
JS_AddIntrinsicMapSet(js);
JS_AddIntrinsicTypedArrays(js);
JS_AddIntrinsicBigInt(js);
JS_AddIntrinsicBigFloat(js);
JS_AddIntrinsicBigDecimal(js);
JS_AddIntrinsicOperators(js);
JS_EnableBignumExt(js, 1);
on_exception = JS_UNDEFINED;
ffi_load(js, argc, argv);
PHYSFS_File *eng = PHYSFS_openRead(ENGINE);
if (!eng) {
printf("Could not open file! %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
return;
}
PHYSFS_Stat stat;
PHYSFS_stat(ENGINE, &stat);
void *data = malloc(stat.filesize);
PHYSFS_readBytes(eng,data,stat.filesize);
PHYSFS_close(eng);
JSValue v = script_eval(js, ENGINE, data);
uncaught_exception(js,v);
free(eng);
}
void script_stop()
{
return;
JS_FreeContext(js);
JS_FreeRuntime(rt);
JS_FreeValue(js,on_exception);
rt = NULL;
js = NULL;
}
void uncaught_exception(JSContext *js, JSValue v)
{
if (!JS_IsException(v)) {
JS_FreeValue(js,v);
return;
}
if (!JS_IsUndefined(on_exception)) {
JSValue ex = JS_GetException(js);
JSValue ret = JS_Call(js, on_exception, JS_UNDEFINED, 1, &ex);
JS_FreeValue(js,ret);
JS_FreeValue(js,ex);
} else {
JSValue ex = JS_GetException(js);
const char *strex = JS_ToCString(js,ex);
JSValue stack = JS_GetPropertyStr(js,ex,"stack");
const char *st = JS_ToCString(js,stack);
printf("Unhandled exception: %s\n%s\n", strex, st);
JS_FreeValue(js,stack);
JS_FreeValue(js,ex);
JS_FreeCString(js,st);
JS_FreeCString(js,strex);
}
JS_FreeValue(js,v);
}
void script_evalf(const char *format, ...)
{
JSValue obj;
va_list args;
va_start(args, format);
int len = vsnprintf(NULL, 0, format, args);
va_end(args);
char *eval = malloc(len+1);
va_start(args, format);
vsnprintf(eval, len+1, format, args);
va_end(args);
obj = JS_Eval(js, eval, len, "C eval", JS_EVAL_FLAGS);
free(eval);
uncaught_exception(js,obj);
}
JSValue script_eval(JSContext *js, const char *file, const char *script)
{
return JS_Eval(js, script, strlen(script), file, JS_EVAL_FLAGS);
}

View File

@@ -1,16 +0,0 @@
#ifndef SCRIPT_H
#define SCRIPT_H
#include "quickjs.h"
extern JSValue on_exception;
void script_startup();
void script_stop();
void script_evalf(const char *format, ...);
JSValue script_eval(JSContext *js, const char *file, const char *script);
void uncaught_exception(JSContext *js, JSValue v);
extern JSContext *global_js;
#endif

View File

@@ -1,17 +1,8 @@
#include "sprite.h" #include "sprite.h"
static sprite model = { sprite *make_sprite(void)
.affine = {.x = 0, .y = 0, .w = 0, .h = 0},
.tex = NULL,
.uv = {.x = 0, .y = 0, .w = 1, .h = 1},
.layer = 0,
.color = {1, 1, 1, 1}
};
sprite *make_sprite()
{ {
sprite *sprite = malloc(sizeof(*sprite)); sprite *sprite = calloc(sizeof(*sprite),1);
*sprite = model;
sprite->image = JS_UNDEFINED; sprite->image = JS_UNDEFINED;
return sprite; return sprite;
} }
@@ -21,3 +12,18 @@ void sprite_free(JSRuntime *rt, sprite *sprite)
JS_FreeValueRT(rt,sprite->image); JS_FreeValueRT(rt,sprite->image);
free(sprite); free(sprite);
} }
void sprite_apply(sprite *sp)
{
/* -------- rotation + skew → 2×2 affine matrix -------- */
float rot = sp->rotation;
HMM_Vec2 k = sp->skew;
float c = cosf(rot), s = sinf(rot);
sp->affine.Columns[0] = (HMM_Vec2){ .x = c + k.x * s,
.y = k.y * c + s };
sp->affine.Columns[1] = (HMM_Vec2){ .x = -s + k.x * c,
.y = -k.y * s + c };
}

View File

@@ -2,21 +2,25 @@
#define SPRITE_H #define SPRITE_H
#include "HandmadeMath.h" #include "HandmadeMath.h"
#include "script.h"
#include "render.h" #include "render.h"
#include "quickjs.h"
struct sprite{ struct sprite{
rect affine; HMM_Vec2 pos; // x,y coordinates of the sprite
JSValue image; HMM_Vec2 center;
SDL_GPUTexture *tex; HMM_Vec2 skew;
rect uv; HMM_Vec2 scale;
int layer; float rotation;
HMM_Vec4 color; HMM_Mat2 affine;
JSValue image; // the JS image
int layer; // layer this sprite draws onto
HMM_Vec4 color; // color in 0-1f
}; };
typedef struct sprite sprite; typedef struct sprite sprite;
sprite *make_sprite(); sprite *make_sprite(void);
void sprite_free(JSRuntime *rt, sprite *sprite); void sprite_free(JSRuntime *rt, sprite *sprite);
void sprite_apply(sprite *sprite);
#endif #endif

View File

@@ -553,8 +553,6 @@ static bool DrawNode(ImDrawList* drawList,
// test nested IO // test nested IO
drawList->ChannelsSetCurrent(1); // Background drawList->ChannelsSetCurrent(1); // Background
const size_t InputsCount = nodeTemplate.mInputCount;
const size_t OutputsCount = nodeTemplate.mOutputCount;
/* /*
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
@@ -647,7 +645,6 @@ static bool DrawNode(ImDrawList* drawList,
drawList->AddLine(progressLineA, progressLineB, 0xFF400000, 3.f); drawList->AddLine(progressLineA, progressLineB, 0xFF400000, 3.f);
drawList->AddLine(progressLineA, ImLerp(progressLineA, progressLineB, progress), 0xFFFF0000, 3.f); drawList->AddLine(progressLineA, ImLerp(progressLineA, progressLineB, progress), 0xFFFF0000, 3.f);
}*/ }*/
ImVec2 imgPosMax = imgPos + ImVec2(imgSizeComp, imgSizeComp);
//ImVec2 imageSize = delegate->GetEvaluationSize(nodeIndex); //ImVec2 imageSize = delegate->GetEvaluationSize(nodeIndex);
/*float imageRatio = 1.f; /*float imageRatio = 1.f;
@@ -828,7 +825,6 @@ void Show(Delegate& delegate, const Options& options, ViewState& viewState, bool
const ImVec2 windowPos = ImGui::GetCursorScreenPos(); const ImVec2 windowPos = ImGui::GetCursorScreenPos();
const ImVec2 canvasSize = ImGui::GetContentRegionAvail(); const ImVec2 canvasSize = ImGui::GetContentRegionAvail();
const ImVec2 scrollRegionLocalPos(0, 0);
ImRect regionRect(windowPos, windowPos + canvasSize); ImRect regionRect(windowPos, windowPos + canvasSize);

View File

@@ -101,16 +101,16 @@ par_shapes_mesh* par_shapes_create_parametric(par_shapes_fn, int slices,
// Generate points for a 20-sided polyhedron that fits in the unit sphere. // Generate points for a 20-sided polyhedron that fits in the unit sphere.
// Texture coordinates and normals are not generated. // Texture coordinates and normals are not generated.
par_shapes_mesh* par_shapes_create_icosahedron(); par_shapes_mesh* par_shapes_create_icosahedron(void);
// Generate points for a 12-sided polyhedron that fits in the unit sphere. // Generate points for a 12-sided polyhedron that fits in the unit sphere.
// Again, texture coordinates and normals are not generated. // Again, texture coordinates and normals are not generated.
par_shapes_mesh* par_shapes_create_dodecahedron(); par_shapes_mesh* par_shapes_create_dodecahedron(void);
// More platonic solids. // More platonic solids.
par_shapes_mesh* par_shapes_create_octahedron(); par_shapes_mesh* par_shapes_create_octahedron(void);
par_shapes_mesh* par_shapes_create_tetrahedron(); par_shapes_mesh* par_shapes_create_tetrahedron(void);
par_shapes_mesh* par_shapes_create_cube(); par_shapes_mesh* par_shapes_create_cube(void);
// Generate an orientable disk shape in 3-space. Does not include normals or // Generate an orientable disk shape in 3-space. Does not include normals or
// texture coordinates. // texture coordinates.
@@ -118,7 +118,7 @@ par_shapes_mesh* par_shapes_create_disk(float radius, int slices,
float const* center, float const* normal); float const* center, float const* normal);
// Create an empty shape. Useful for building scenes with merge_and_free. // Create an empty shape. Useful for building scenes with merge_and_free.
par_shapes_mesh* par_shapes_create_empty(); par_shapes_mesh* par_shapes_create_empty(void);
// Generate a rock shape that sits on the Y=0 plane, and sinks into it a bit. // Generate a rock shape that sits on the Y=0 plane, and sinks into it a bit.
// This includes smooth normals but no texture coordinates. Each subdivision // This includes smooth normals but no texture coordinates. Each subdivision
@@ -751,7 +751,7 @@ par_shapes_mesh* par_shapes_create_disk(float radius, int slices,
return mesh; return mesh;
} }
par_shapes_mesh* par_shapes_create_empty() par_shapes_mesh* par_shapes_create_empty(void)
{ {
return PAR_CALLOC(par_shapes_mesh, 1); return PAR_CALLOC(par_shapes_mesh, 1);
} }
@@ -875,7 +875,7 @@ void par_shapes_invert(par_shapes_mesh* m, int face, int nfaces)
} }
} }
par_shapes_mesh* par_shapes_create_icosahedron() par_shapes_mesh* par_shapes_create_icosahedron(void)
{ {
static float verts[] = { static float verts[] = {
0.000, 0.000, 1.000, 0.000, 0.000, 1.000,
@@ -923,7 +923,7 @@ par_shapes_mesh* par_shapes_create_icosahedron()
return mesh; return mesh;
} }
par_shapes_mesh* par_shapes_create_dodecahedron() par_shapes_mesh* par_shapes_create_dodecahedron(void)
{ {
static float verts[20 * 3] = { static float verts[20 * 3] = {
0.607, 0.000, 0.795, 0.607, 0.000, 0.795,
@@ -985,7 +985,7 @@ par_shapes_mesh* par_shapes_create_dodecahedron()
return mesh; return mesh;
} }
par_shapes_mesh* par_shapes_create_octahedron() par_shapes_mesh* par_shapes_create_octahedron(void)
{ {
static float verts[6 * 3] = { static float verts[6 * 3] = {
0.000, 0.000, 1.000, 0.000, 0.000, 1.000,
@@ -1023,7 +1023,7 @@ par_shapes_mesh* par_shapes_create_octahedron()
return mesh; return mesh;
} }
par_shapes_mesh* par_shapes_create_tetrahedron() par_shapes_mesh* par_shapes_create_tetrahedron(void)
{ {
static float verts[4 * 3] = { static float verts[4 * 3] = {
0.000, 1.333, 0, 0.000, 1.333, 0,
@@ -1055,7 +1055,7 @@ par_shapes_mesh* par_shapes_create_tetrahedron()
return mesh; return mesh;
} }
par_shapes_mesh* par_shapes_create_cube() par_shapes_mesh* par_shapes_create_cube(void)
{ {
static float verts[8 * 3] = { static float verts[8 * 3] = {
0, 0, 0, // 0 0, 0, 0, // 0
@@ -1141,7 +1141,7 @@ static par_shapes__rule* par_shapes__pick_rule(const char* name,
return rule; return rule;
} }
static par_shapes_mesh* par_shapes__create_turtle() static par_shapes_mesh* par_shapes__create_turtle(void)
{ {
const float xaxis[] = {1, 0, 0}; const float xaxis[] = {1, 0, 0};
const float yaxis[] = {0, 1, 0}; const float yaxis[] = {0, 1, 0};

948
source/thirdparty/quirc/decode.c vendored Normal file
View File

@@ -0,0 +1,948 @@
/* quirc -- QR-code recognition library
* Copyright (C) 2010-2012 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "quirc_internal.h"
#include <string.h>
#include <stdlib.h>
#define MAX_POLY 64
/************************************************************************
* Galois fields
*/
struct galois_field {
int p;
const uint8_t *log;
const uint8_t *exp;
};
static const uint8_t gf16_exp[16] = {
0x01, 0x02, 0x04, 0x08, 0x03, 0x06, 0x0c, 0x0b,
0x05, 0x0a, 0x07, 0x0e, 0x0f, 0x0d, 0x09, 0x01
};
static const uint8_t gf16_log[16] = {
0x00, 0x0f, 0x01, 0x04, 0x02, 0x08, 0x05, 0x0a,
0x03, 0x0e, 0x09, 0x07, 0x06, 0x0d, 0x0b, 0x0c
};
static const struct galois_field gf16 = {
.p = 15,
.log = gf16_log,
.exp = gf16_exp
};
static const uint8_t gf256_exp[256] = {
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9,
0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35,
0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0,
0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc,
0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f,
0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88,
0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93,
0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9,
0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa,
0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e,
0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4,
0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e,
0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef,
0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5,
0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83,
0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x01
};
static const uint8_t gf256_log[256] = {
0x00, 0xff, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6,
0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81,
0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21,
0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9,
0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd,
0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd,
0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e,
0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b,
0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d,
0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c,
0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd,
0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e,
0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76,
0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa,
0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51,
0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8,
0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
};
static const struct galois_field gf256 = {
.p = 255,
.log = gf256_log,
.exp = gf256_exp
};
/************************************************************************
* Polynomial operations
*/
static void poly_add(uint8_t *dst, const uint8_t *src, uint8_t c,
int shift, const struct galois_field *gf)
{
int i;
int log_c = gf->log[c];
if (!c)
return;
for (i = 0; i < MAX_POLY; i++) {
int p = i + shift;
uint8_t v = src[i];
if (p < 0 || p >= MAX_POLY)
continue;
if (!v)
continue;
dst[p] ^= gf->exp[(gf->log[v] + log_c) % gf->p];
}
}
static uint8_t poly_eval(const uint8_t *s, uint8_t x,
const struct galois_field *gf)
{
int i;
uint8_t sum = 0;
uint8_t log_x = gf->log[x];
if (!x)
return s[0];
for (i = 0; i < MAX_POLY; i++) {
uint8_t c = s[i];
if (!c)
continue;
sum ^= gf->exp[(gf->log[c] + log_x * i) % gf->p];
}
return sum;
}
/************************************************************************
* Berlekamp-Massey algorithm for finding error locator polynomials.
*/
static void berlekamp_massey(const uint8_t *s, int N,
const struct galois_field *gf,
uint8_t *sigma)
{
uint8_t C[MAX_POLY];
uint8_t B[MAX_POLY];
int L = 0;
int m = 1;
uint8_t b = 1;
int n;
memset(B, 0, sizeof(B));
memset(C, 0, sizeof(C));
B[0] = 1;
C[0] = 1;
for (n = 0; n < N; n++) {
uint8_t d = s[n];
uint8_t mult;
int i;
for (i = 1; i <= L; i++) {
if (!(C[i] && s[n - i]))
continue;
d ^= gf->exp[(gf->log[C[i]] +
gf->log[s[n - i]]) %
gf->p];
}
mult = gf->exp[(gf->p - gf->log[b] + gf->log[d]) % gf->p];
if (!d) {
m++;
} else if (L * 2 <= n) {
uint8_t T[MAX_POLY];
memcpy(T, C, sizeof(T));
poly_add(C, B, mult, m, gf);
memcpy(B, T, sizeof(B));
L = n + 1 - L;
b = d;
m = 1;
} else {
poly_add(C, B, mult, m, gf);
m++;
}
}
memcpy(sigma, C, MAX_POLY);
}
/************************************************************************
* Code stream error correction
*
* Generator polynomial for GF(2^8) is x^8 + x^4 + x^3 + x^2 + 1
*/
static int block_syndromes(const uint8_t *data, int bs, int npar, uint8_t *s)
{
int nonzero = 0;
int i;
memset(s, 0, MAX_POLY);
for (i = 0; i < npar; i++) {
int j;
for (j = 0; j < bs; j++) {
uint8_t c = data[bs - j - 1];
if (!c)
continue;
s[i] ^= gf256_exp[((int)gf256_log[c] +
i * j) % 255];
}
if (s[i])
nonzero = 1;
}
return nonzero;
}
static void eloc_poly(uint8_t *omega,
const uint8_t *s, const uint8_t *sigma,
int npar)
{
int i;
memset(omega, 0, MAX_POLY);
for (i = 0; i < npar; i++) {
const uint8_t a = sigma[i];
const uint8_t log_a = gf256_log[a];
int j;
if (!a)
continue;
for (j = 0; j + 1 < MAX_POLY; j++) {
const uint8_t b = s[j + 1];
if (i + j >= npar)
break;
if (!b)
continue;
omega[i + j] ^=
gf256_exp[(log_a + gf256_log[b]) % 255];
}
}
}
static quirc_decode_error_t correct_block(uint8_t *data,
const struct quirc_rs_params *ecc)
{
int npar = ecc->bs - ecc->dw;
uint8_t s[MAX_POLY];
uint8_t sigma[MAX_POLY];
uint8_t sigma_deriv[MAX_POLY];
uint8_t omega[MAX_POLY];
int i;
/* Compute syndrome vector */
if (!block_syndromes(data, ecc->bs, npar, s))
return QUIRC_SUCCESS;
berlekamp_massey(s, npar, &gf256, sigma);
/* Compute derivative of sigma */
memset(sigma_deriv, 0, MAX_POLY);
for (i = 0; i + 1 < MAX_POLY; i += 2)
sigma_deriv[i] = sigma[i + 1];
/* Compute error evaluator polynomial */
eloc_poly(omega, s, sigma, npar - 1);
/* Find error locations and magnitudes */
for (i = 0; i < ecc->bs; i++) {
uint8_t xinv = gf256_exp[255 - i];
if (!poly_eval(sigma, xinv, &gf256)) {
uint8_t sd_x = poly_eval(sigma_deriv, xinv, &gf256);
uint8_t omega_x = poly_eval(omega, xinv, &gf256);
uint8_t error = gf256_exp[(255 - gf256_log[sd_x] +
gf256_log[omega_x]) % 255];
data[ecc->bs - i - 1] ^= error;
}
}
if (block_syndromes(data, ecc->bs, npar, s))
return QUIRC_ERROR_DATA_ECC;
return QUIRC_SUCCESS;
}
/************************************************************************
* Format value error correction
*
* Generator polynomial for GF(2^4) is x^4 + x + 1
*/
#define FORMAT_MAX_ERROR 3
#define FORMAT_SYNDROMES (FORMAT_MAX_ERROR * 2)
#define FORMAT_BITS 15
static int format_syndromes(uint16_t u, uint8_t *s)
{
int i;
int nonzero = 0;
memset(s, 0, MAX_POLY);
for (i = 0; i < FORMAT_SYNDROMES; i++) {
int j;
s[i] = 0;
for (j = 0; j < FORMAT_BITS; j++)
if (u & (1 << j))
s[i] ^= gf16_exp[((i + 1) * j) % 15];
if (s[i])
nonzero = 1;
}
return nonzero;
}
static quirc_decode_error_t correct_format(uint16_t *f_ret)
{
uint16_t u = *f_ret;
int i;
uint8_t s[MAX_POLY];
uint8_t sigma[MAX_POLY];
/* Evaluate U (received codeword) at each of alpha_1 .. alpha_6
* to get S_1 .. S_6 (but we index them from 0).
*/
if (!format_syndromes(u, s))
return QUIRC_SUCCESS;
berlekamp_massey(s, FORMAT_SYNDROMES, &gf16, sigma);
/* Now, find the roots of the polynomial */
for (i = 0; i < 15; i++)
if (!poly_eval(sigma, gf16_exp[15 - i], &gf16))
u ^= (1 << i);
if (format_syndromes(u, s))
return QUIRC_ERROR_FORMAT_ECC;
*f_ret = u;
return QUIRC_SUCCESS;
}
/************************************************************************
* Decoder algorithm
*/
struct datastream {
uint8_t *raw;
int data_bits;
int ptr;
uint8_t data[QUIRC_MAX_PAYLOAD];
};
static inline int grid_bit(const struct quirc_code *code, int x, int y)
{
int p = y * code->size + x;
return (code->cell_bitmap[p >> 3] >> (p & 7)) & 1;
}
static quirc_decode_error_t read_format(const struct quirc_code *code,
struct quirc_data *data, int which)
{
int i;
uint16_t format = 0;
uint16_t fdata;
quirc_decode_error_t err;
if (which) {
for (i = 0; i < 7; i++)
format = (format << 1) |
grid_bit(code, 8, code->size - 1 - i);
for (i = 0; i < 8; i++)
format = (format << 1) |
grid_bit(code, code->size - 8 + i, 8);
} else {
static const int xs[15] = {
8, 8, 8, 8, 8, 8, 8, 8, 7, 5, 4, 3, 2, 1, 0
};
static const int ys[15] = {
0, 1, 2, 3, 4, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8
};
for (i = 14; i >= 0; i--)
format = (format << 1) | grid_bit(code, xs[i], ys[i]);
}
format ^= 0x5412;
err = correct_format(&format);
if (err)
return err;
fdata = format >> 10;
data->ecc_level = fdata >> 3;
data->mask = fdata & 7;
return QUIRC_SUCCESS;
}
static int mask_bit(int mask, int i, int j)
{
switch (mask) {
case 0: return !((i + j) % 2);
case 1: return !(i % 2);
case 2: return !(j % 3);
case 3: return !((i + j) % 3);
case 4: return !(((i / 2) + (j / 3)) % 2);
case 5: return !((i * j) % 2 + (i * j) % 3);
case 6: return !(((i * j) % 2 + (i * j) % 3) % 2);
case 7: return !(((i * j) % 3 + (i + j) % 2) % 2);
}
return 0;
}
static int reserved_cell(int version, int i, int j)
{
const struct quirc_version_info *ver = &quirc_version_db[version];
int size = version * 4 + 17;
int ai = -1, aj = -1, a;
/* Finder + format: top left */
if (i < 9 && j < 9)
return 1;
/* Finder + format: bottom left */
if (i + 8 >= size && j < 9)
return 1;
/* Finder + format: top right */
if (i < 9 && j + 8 >= size)
return 1;
/* Exclude timing patterns */
if (i == 6 || j == 6)
return 1;
/* Exclude version info, if it exists. Version info sits adjacent to
* the top-right and bottom-left finders in three rows, bounded by
* the timing pattern.
*/
if (version >= 7) {
if (i < 6 && j + 11 >= size)
return 1;
if (i + 11 >= size && j < 6)
return 1;
}
/* Exclude alignment patterns */
for (a = 0; a < QUIRC_MAX_ALIGNMENT && ver->apat[a]; a++) {
int p = ver->apat[a];
if (abs(p - i) < 3)
ai = a;
if (abs(p - j) < 3)
aj = a;
}
if (ai >= 0 && aj >= 0) {
a--;
if (ai > 0 && ai < a)
return 1;
if (aj > 0 && aj < a)
return 1;
if (aj == a && ai == a)
return 1;
}
return 0;
}
static void read_bit(const struct quirc_code *code,
struct quirc_data *data,
struct datastream *ds, int i, int j)
{
int bitpos = ds->data_bits & 7;
int bytepos = ds->data_bits >> 3;
int v = grid_bit(code, j, i);
if (mask_bit(data->mask, i, j))
v ^= 1;
if (v)
ds->raw[bytepos] |= (0x80 >> bitpos);
ds->data_bits++;
}
static void read_data(const struct quirc_code *code,
struct quirc_data *data,
struct datastream *ds)
{
int y = code->size - 1;
int x = code->size - 1;
int dir = -1;
while (x > 0) {
if (x == 6)
x--;
if (!reserved_cell(data->version, y, x))
read_bit(code, data, ds, y, x);
if (!reserved_cell(data->version, y, x - 1))
read_bit(code, data, ds, y, x - 1);
y += dir;
if (y < 0 || y >= code->size) {
dir = -dir;
x -= 2;
y += dir;
}
}
}
static quirc_decode_error_t codestream_ecc(struct quirc_data *data,
struct datastream *ds)
{
const struct quirc_version_info *ver =
&quirc_version_db[data->version];
const struct quirc_rs_params *sb_ecc = &ver->ecc[data->ecc_level];
struct quirc_rs_params lb_ecc;
const int lb_count =
(ver->data_bytes - sb_ecc->bs * sb_ecc->ns) / (sb_ecc->bs + 1);
const int bc = lb_count + sb_ecc->ns;
const int ecc_offset = sb_ecc->dw * bc + lb_count;
int dst_offset = 0;
int i;
memcpy(&lb_ecc, sb_ecc, sizeof(lb_ecc));
lb_ecc.dw++;
lb_ecc.bs++;
for (i = 0; i < bc; i++) {
uint8_t *dst = ds->data + dst_offset;
const struct quirc_rs_params *ecc =
(i < sb_ecc->ns) ? sb_ecc : &lb_ecc;
const int num_ec = ecc->bs - ecc->dw;
quirc_decode_error_t err;
int j;
for (j = 0; j < ecc->dw; j++)
dst[j] = ds->raw[j * bc + i];
for (j = 0; j < num_ec; j++)
dst[ecc->dw + j] = ds->raw[ecc_offset + j * bc + i];
err = correct_block(dst, ecc);
if (err)
return err;
dst_offset += ecc->dw;
}
ds->data_bits = dst_offset * 8;
return QUIRC_SUCCESS;
}
static inline int bits_remaining(const struct datastream *ds)
{
return ds->data_bits - ds->ptr;
}
static int take_bits(struct datastream *ds, int len)
{
int ret = 0;
while (len && (ds->ptr < ds->data_bits)) {
uint8_t b = ds->data[ds->ptr >> 3];
int bitpos = ds->ptr & 7;
ret <<= 1;
if ((b << bitpos) & 0x80)
ret |= 1;
ds->ptr++;
len--;
}
return ret;
}
static int numeric_tuple(struct quirc_data *data,
struct datastream *ds,
int bits, int digits)
{
int tuple;
int i;
if (bits_remaining(ds) < bits)
return -1;
tuple = take_bits(ds, bits);
for (i = digits - 1; i >= 0; i--) {
data->payload[data->payload_len + i] = tuple % 10 + '0';
tuple /= 10;
}
data->payload_len += digits;
return 0;
}
static quirc_decode_error_t decode_numeric(struct quirc_data *data,
struct datastream *ds)
{
int bits = 14;
int count;
if (data->version < 10)
bits = 10;
else if (data->version < 27)
bits = 12;
count = take_bits(ds, bits);
if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD)
return QUIRC_ERROR_DATA_OVERFLOW;
while (count >= 3) {
if (numeric_tuple(data, ds, 10, 3) < 0)
return QUIRC_ERROR_DATA_UNDERFLOW;
count -= 3;
}
if (count >= 2) {
if (numeric_tuple(data, ds, 7, 2) < 0)
return QUIRC_ERROR_DATA_UNDERFLOW;
count -= 2;
}
if (count) {
if (numeric_tuple(data, ds, 4, 1) < 0)
return QUIRC_ERROR_DATA_UNDERFLOW;
count--;
}
return QUIRC_SUCCESS;
}
static int alpha_tuple(struct quirc_data *data,
struct datastream *ds,
int bits, int digits)
{
int tuple;
int i;
if (bits_remaining(ds) < bits)
return -1;
tuple = take_bits(ds, bits);
for (i = 0; i < digits; i++) {
static const char *alpha_map =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
data->payload[data->payload_len + digits - i - 1] =
alpha_map[tuple % 45];
tuple /= 45;
}
data->payload_len += digits;
return 0;
}
static quirc_decode_error_t decode_alpha(struct quirc_data *data,
struct datastream *ds)
{
int bits = 13;
int count;
if (data->version < 10)
bits = 9;
else if (data->version < 27)
bits = 11;
count = take_bits(ds, bits);
if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD)
return QUIRC_ERROR_DATA_OVERFLOW;
while (count >= 2) {
if (alpha_tuple(data, ds, 11, 2) < 0)
return QUIRC_ERROR_DATA_UNDERFLOW;
count -= 2;
}
if (count) {
if (alpha_tuple(data, ds, 6, 1) < 0)
return QUIRC_ERROR_DATA_UNDERFLOW;
count--;
}
return QUIRC_SUCCESS;
}
static quirc_decode_error_t decode_byte(struct quirc_data *data,
struct datastream *ds)
{
int bits = 16;
int count;
int i;
if (data->version < 10)
bits = 8;
count = take_bits(ds, bits);
if (data->payload_len + count + 1 > QUIRC_MAX_PAYLOAD)
return QUIRC_ERROR_DATA_OVERFLOW;
if (bits_remaining(ds) < count * 8)
return QUIRC_ERROR_DATA_UNDERFLOW;
for (i = 0; i < count; i++)
data->payload[data->payload_len++] = take_bits(ds, 8);
return QUIRC_SUCCESS;
}
static quirc_decode_error_t decode_kanji(struct quirc_data *data,
struct datastream *ds)
{
int bits = 12;
int count;
int i;
if (data->version < 10)
bits = 8;
else if (data->version < 27)
bits = 10;
count = take_bits(ds, bits);
if (data->payload_len + count * 2 + 1 > QUIRC_MAX_PAYLOAD)
return QUIRC_ERROR_DATA_OVERFLOW;
if (bits_remaining(ds) < count * 13)
return QUIRC_ERROR_DATA_UNDERFLOW;
for (i = 0; i < count; i++) {
int d = take_bits(ds, 13);
int msB = d / 0xc0;
int lsB = d % 0xc0;
int intermediate = (msB << 8) | lsB;
uint16_t sjw;
if (intermediate + 0x8140 <= 0x9ffc) {
/* bytes are in the range 0x8140 to 0x9FFC */
sjw = intermediate + 0x8140;
} else {
/* bytes are in the range 0xE040 to 0xEBBF */
sjw = intermediate + 0xc140;
}
data->payload[data->payload_len++] = sjw >> 8;
data->payload[data->payload_len++] = sjw & 0xff;
}
return QUIRC_SUCCESS;
}
static quirc_decode_error_t decode_eci(struct quirc_data *data,
struct datastream *ds)
{
if (bits_remaining(ds) < 8)
return QUIRC_ERROR_DATA_UNDERFLOW;
data->eci = take_bits(ds, 8);
if ((data->eci & 0xc0) == 0x80) {
if (bits_remaining(ds) < 8)
return QUIRC_ERROR_DATA_UNDERFLOW;
data->eci = (data->eci << 8) | take_bits(ds, 8);
} else if ((data->eci & 0xe0) == 0xc0) {
if (bits_remaining(ds) < 16)
return QUIRC_ERROR_DATA_UNDERFLOW;
data->eci = (data->eci << 16) | take_bits(ds, 16);
}
return QUIRC_SUCCESS;
}
static quirc_decode_error_t decode_payload(struct quirc_data *data,
struct datastream *ds)
{
while (bits_remaining(ds) >= 4) {
quirc_decode_error_t err = QUIRC_SUCCESS;
int type = take_bits(ds, 4);
switch (type) {
case QUIRC_DATA_TYPE_NUMERIC:
err = decode_numeric(data, ds);
break;
case QUIRC_DATA_TYPE_ALPHA:
err = decode_alpha(data, ds);
break;
case QUIRC_DATA_TYPE_BYTE:
err = decode_byte(data, ds);
break;
case QUIRC_DATA_TYPE_KANJI:
err = decode_kanji(data, ds);
break;
case 7:
err = decode_eci(data, ds);
break;
default:
goto done;
}
if (err)
return err;
if (!(type & (type - 1)) && (type > data->data_type))
data->data_type = type;
}
done:
/* Add nul terminator to all payloads */
if (data->payload_len >= (int) sizeof(data->payload))
data->payload_len--;
data->payload[data->payload_len] = 0;
return QUIRC_SUCCESS;
}
quirc_decode_error_t quirc_decode(const struct quirc_code *code,
struct quirc_data *data)
{
quirc_decode_error_t err;
struct datastream ds;
if (code->size > QUIRC_MAX_GRID_SIZE)
return QUIRC_ERROR_INVALID_GRID_SIZE;
if ((code->size - 17) % 4)
return QUIRC_ERROR_INVALID_GRID_SIZE;
memset(data, 0, sizeof(*data));
memset(&ds, 0, sizeof(ds));
data->version = (code->size - 17) / 4;
if (data->version < 1 ||
data->version > QUIRC_MAX_VERSION)
return QUIRC_ERROR_INVALID_VERSION;
/* Read format information -- try both locations */
err = read_format(code, data, 0);
if (err)
err = read_format(code, data, 1);
if (err)
return err;
/*
* Borrow data->payload to store the raw bits.
* It's only used during read_data + coddestream_ecc below.
*
* This trick saves the size of struct datastream, which we allocate
* on the stack.
*/
ds.raw = data->payload;
read_data(code, data, &ds);
err = codestream_ecc(data, &ds);
if (err)
return err;
ds.raw = NULL; /* We've done with this buffer. */
err = decode_payload(data, &ds);
if (err)
return err;
return QUIRC_SUCCESS;
}
void quirc_flip(struct quirc_code *code)
{
struct quirc_code flipped = {0};
unsigned int offset = 0;
for (int y = 0; y < code->size; y++) {
for (int x = 0; x < code->size; x++) {
if (grid_bit(code, y, x)) {
flipped.cell_bitmap[offset >> 3u] |= (1u << (offset & 7u));
}
offset++;
}
}
memcpy(&code->cell_bitmap, &flipped.cell_bitmap, sizeof(flipped.cell_bitmap));
}

1153
source/thirdparty/quirc/identify.c vendored Normal file

File diff suppressed because it is too large Load Diff

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