Compare commits
1084 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bab9462409 | |||
| 5370507859 | |||
| eeb7a3d5fb | |||
| 9883008cd1 | |||
| e72a4d0a13 | |||
| 737d6e1859 | |||
| 8a997fd7ce | |||
| 754faa7730 | |||
| 67d477af77 | |||
| 314b8ce194 | |||
| 2a18d9b51f | |||
| d7adc28125 | |||
| e4265f5fdb | |||
| a3e3ea2f8e | |||
| a637aec63e | |||
| ab0b84b87e | |||
| d22abae476 | |||
| 9ce51c2d49 | |||
| 7918103b8b | |||
| 176dc79b49 | |||
| 87b95625ff | |||
| 530956889b | |||
| 54e267dfa6 | |||
| e535879262 | |||
| 740ac2a879 | |||
| 5d9f9cf284 | |||
| a6ce456c60 | |||
| c18579f777 | |||
| ad7cbd8e2e | |||
| 69adbddb46 | |||
| 42147937b2 | |||
| 56762050fb | |||
| 744a03bdfa | |||
| 8882331421 | |||
| e2aa928e95 | |||
| 25a57c099b | |||
| 490dc0f563 | |||
| 130b9091d1 | |||
| 2288feeb59 | |||
| 96798c0fbb | |||
| 9a1cae1d52 | |||
| c2fc8f78f2 | |||
| 12553edb26 | |||
| ea451dfda2 | |||
| 7c4e6870b3 | |||
| 986fba3586 | |||
| 00b45742d1 | |||
| 81e1f7d8e6 | |||
| 3bf07ce606 | |||
| d4df925b8f | |||
| 26b986de5b | |||
| 64adb052b4 | |||
| d703d79feb | |||
| 96c6e27d98 | |||
| a3377215e9 | |||
| 01df9b9d77 | |||
| 47ba741f6d | |||
| c3a37bf76c | |||
| c9cf202721 | |||
| 1ab5ca14fb | |||
| 10a394ae6c | |||
| a1419cdd1f | |||
| 2e0a7f0031 | |||
| da78010281 | |||
| 603a5bd63f | |||
| 9f1273fb32 | |||
| 9a0011c38f | |||
| bdda22cb2e | |||
| 86644e6533 | |||
| eaeadc6a52 | |||
| eb0d3b1846 | |||
| 50333571d7 | |||
| 320ce42626 | |||
| 047bdf4189 | |||
| d39a27a904 | |||
| f0cf85943f | |||
| bfd99cdda1 | |||
| 9391695fce | |||
| 3d65ec6def | |||
| c804c4b0ee | |||
| 86146782f7 | |||
| ed19fb5ce4 | |||
| 9f8aec3bc1 | |||
| 4ccbc12b48 | |||
| 86c46c629b | |||
| e2528c1b10 | |||
| eb2d6106af | |||
| 2ad6edaae3 | |||
| 73f7d0b7a3 | |||
| 27024de00e | |||
| 11447b8650 | |||
| cdaeb33e12 | |||
| 9f9ac4a7ce | |||
| e3788c0a43 | |||
| 66b8e51026 | |||
| f9808e31ec | |||
| 4988adb170 | |||
| cc1ea0f4ae | |||
| 12e2816480 | |||
| 92530c5db7 | |||
| 2fd01d0c9f | |||
| f0096aec87 | |||
| b5b3edc6ec | |||
| 82c652ca31 | |||
| b3076597e5 | |||
| 31dcc389a2 | |||
| 78c261c625 | |||
| 5107d501f5 | |||
| aad3f8ca27 | |||
| 5bc2342174 | |||
| 9f07fc8a62 | |||
| a8ccd34559 | |||
| ef8bf09210 | |||
| 2161f28766 | |||
| cebb78c2c3 | |||
| edc94e49e8 | |||
| 03ecffbf52 | |||
| f864f94207 | |||
| af90a1a334 | |||
| c3fc896dea | |||
| 9cc67fee74 | |||
| 847048825a | |||
| 55def863d3 | |||
| 0a6485ca71 | |||
| aac58ec6fa | |||
| 37338cb0f7 | |||
| 3d5aaa6f6e | |||
| 2c800ea84d | |||
| 4db972f4c8 | |||
| 4fb1dc0ddc | |||
| ec49207533 | |||
| ea51307eaa | |||
| 49e32f9b47 | |||
| b053bf625c | |||
| 0634116d59 | |||
| 7c25c48dd2 | |||
| d8baf4e579 | |||
| 9186abbbe3 | |||
| 9ff219d065 | |||
| 4376ddf0ec | |||
| e6f4685bd0 | |||
| 5a751434ac | |||
| 2f4860ae71 | |||
| 7170c31304 | |||
| 313f95cd1f | |||
| 359de7bfe5 | |||
| aba5d9e679 | |||
| cc24409d87 | |||
| d63de113fb | |||
| c672be101e | |||
| 668a9ecf69 | |||
| a87f20c66e | |||
| a22b8a572b | |||
| e84a59aeff | |||
| dd25267502 | |||
| a436cfbe35 | |||
| e74656c372 | |||
| 49b5e80af5 | |||
| 281ebc23e1 | |||
| 28f7b4666b | |||
| e53a9fc865 | |||
| bda7489fbe | |||
| abc78baad8 | |||
| 3f2723582f | |||
| e908672034 | |||
| f66b912cf4 | |||
| 81cb992fad | |||
| 7f476513cd | |||
| d82dc3162f | |||
| 6c690c5db6 | |||
| 18755d9057 | |||
| dd1e852356 | |||
| dacc0f7280 | |||
| 5517873275 | |||
| d9a73d63bd | |||
| 8acce7beb9 | |||
| f79390838a | |||
| e8d766eafb | |||
| b5bffe99fa | |||
| e62c9de7e9 | |||
| 396f1f3955 | |||
| 5197cf4cb8 | |||
| 48bf705ca4 | |||
| 34c2ac2cb9 | |||
| ebfbabdfd8 | |||
| 6a379ae9aa | |||
| d2e1f804ce | |||
| 64090f7497 | |||
| b647964839 | |||
| 7387f2406a | |||
| e83802f15e | |||
| 863b9712f9 | |||
| 42deb76c28 | |||
| 31df4dba9f | |||
| f8d811aa97 | |||
| 06d828a296 | |||
| f02eab5c51 | |||
| 199109f1ae | |||
| 233bf152b9 | |||
| 82de673ce7 | |||
| 9e6e7df3e6 | |||
| 48681e4295 | |||
| 967f15ded9 | |||
| 305bd294a0 | |||
| 52ce5356cc | |||
| 8589a5a7b4 | |||
| e1585de9b6 | |||
| 3f0bb07c16 | |||
| 104ba696c2 | |||
| a2683fbec2 | |||
| 1fd87c7376 | |||
| 7444638030 | |||
| ba1a7b324e | |||
| 7f3b842d59 | |||
| a12f1b1201 | |||
| c883687ace | |||
| 50ba7401fd | |||
| c592974055 | |||
| 253fe397f4 | |||
| 883ea8d48a | |||
| 7d8882d28d | |||
| d6122f0e31 | |||
| 7ffa0ecf6d | |||
| c775e5b74e | |||
| 617ac10c5a | |||
| 1033c78db3 | |||
| 9cc3f24c56 | |||
| a9b7d216c8 | |||
| 7c74d95efa | |||
| eee6a0906e | |||
| 8c598b18af | |||
| 3254d8e81e | |||
| 329439cf61 | |||
| 9f94be7cca | |||
| 5b3cfac48e | |||
| 306b57e1d5 | |||
| c5e58d14ea | |||
| 509b5ef111 | |||
| b2550ca58a | |||
| f8c81e4066 | |||
| b4af12025d | |||
| 031fcad641 | |||
| 4d485a1778 | |||
| e33a1133b7 | |||
| 7024f42789 | |||
| 41ad5d1e9b | |||
| 181d317d20 | |||
| 5f6277a74c | |||
| 6289564382 | |||
| 6af48afdda | |||
| cdf7da94db | |||
| 8b85282502 | |||
| 93e5cb391e | |||
| d8aa070c93 | |||
| f65bd1aca1 | |||
| ef924b9e9e | |||
| 2ce2d997be | |||
| 2958539514 | |||
| b9f9cb8495 | |||
| 160ce96e4f | |||
| 0c6dab39a8 | |||
| 9b1cd7d608 | |||
| 22a384a76a | |||
| db7c21e5d8 | |||
| 2bb7ae1883 | |||
| e958d00373 | |||
| 5297996640 | |||
| 8030cfdfde | |||
| 84d754a4e9 | |||
| 30a641d249 | |||
| 12fdc0717e | |||
| 5d8cbac06a | |||
| 90b23e11e9 | |||
| 3f685af868 | |||
| 2bd6d30dcc | |||
| 99f8749949 | |||
| 6f39b50e63 | |||
| 338202c41b | |||
| b368912c78 | |||
| 7b91300962 | |||
| f38581754a | |||
| 9b3d3e3298 | |||
| 6029d10e62 | |||
| 614f030407 | |||
| e691f072ae | |||
| a1a90e0626 | |||
| 06ecc5cd8e | |||
| e476a9d975 | |||
| 15f1020fb6 | |||
| a299117858 | |||
| 9c24d707bc | |||
| 1667d7f16e | |||
| 6bd75e3bf2 | |||
| 6a82b4928b | |||
| a64a403def | |||
| 3a2d594626 | |||
| d117f2b8a1 | |||
| 0a239c4bed | |||
| 0b35f810b6 | |||
| 66da424b90 | |||
| b2a45c2fda | |||
| 87587b00b6 | |||
| 28ad6f62ea | |||
| b989f653a0 | |||
| 6ab98292d2 | |||
| 61e057f73e | |||
| 25c8c9279c | |||
| 48a8caa292 | |||
| a99f75f2a4 | |||
| 47bb0e5d4d | |||
| 765d800842 | |||
| 7b20f305ff | |||
| 3816d9924b | |||
| afc2d019a3 | |||
| 0ed66baed0 | |||
| 90dd603cee | |||
| 7187aaa4d5 | |||
| a5f6c02a09 | |||
| f4233ba11d | |||
| 88580ca319 | |||
| beebbd0bc1 | |||
| d4edd0f5ba | |||
| 0f04b63516 | |||
| dba978e22b | |||
| 34421b8015 | |||
| 01c2971ce7 | |||
| b30711877d | |||
| fc99cee9a9 | |||
| 2603a79cc4 | |||
| a0f60e5296 | |||
| 5ffa67b31b | |||
| 1786e144bb | |||
| de6d78bef5 | |||
| 2b02c0a931 | |||
| bd2d62ddca | |||
| 4d3ecfb72c | |||
| d483c9283c | |||
| 8228597ade | |||
| 858544e231 | |||
| 4fc9d2616a | |||
| 3ac1179440 | |||
| b3ce6463b5 | |||
| f993c4a36a | |||
| dc614a6a33 | |||
| 5d34d0fd09 | |||
| 4bca5dfa92 | |||
| 00408604da | |||
| 179dd60536 | |||
| ae68b7f752 | |||
| b12b66e89a | |||
| b513f7b5b1 | |||
| d9612ba2a6 | |||
| 3b9b4a589b | |||
| 5be2422c84 | |||
| 4df15eacfb | |||
| 1e714623b7 | |||
| fc22c3f250 | |||
| dd20cb2d5d | |||
| 8c72437ecc | |||
| 2f6386069d | |||
| ab6b666fd6 | |||
| 91f91716b5 | |||
| 4e0c30488f | |||
| 1381c6c903 | |||
| d59636bcf6 | |||
| 5aa7c03022 | |||
| 2cce41062f | |||
| 07fe90eb5f | |||
| ac8c7b0c0b | |||
| 5e80db327d | |||
| bad7c4a255 | |||
| 1315baf332 | |||
| c8a588b2de | |||
| e91091d62b | |||
| 5a5e5cb258 | |||
| 0a078c7822 | |||
| 1bf3470873 | |||
| bc50bd9b9a | |||
| 8fbfeb7f9d | |||
| a6364199da | |||
| f16ac4ef9e | |||
| c5114a73fc | |||
| b87ee96805 | |||
| 90074bfe60 | |||
| 0b89ed85b7 | |||
| 64b361510d | |||
| a0c6f8ce7f | |||
| e009ea313b | |||
| a8906b4f79 | |||
| 1cc507e181 | |||
| 6f16e96de2 | |||
| cf8cbb3afb | |||
| 2fca1056c6 | |||
| 39af9cabc0 | |||
| 3908163bdf | |||
| 4480353cb2 | |||
| 7d85ea3432 | |||
| 3762453d83 | |||
| 3205408290 | |||
| c6bea826f6 | |||
| 54c0feafc2 | |||
| 46000362ba | |||
| f8825dc55a | |||
| 6fb5118afd | |||
| 23d204b9fc | |||
| 429e419327 | |||
| 69d1db06a5 | |||
| ce2699dfee | |||
| a64713fc23 | |||
| 958a262ba1 | |||
| f7e6859696 | |||
| db3a3d80d1 | |||
| a0b030290b | |||
| 629579123a | |||
| 163aabea6c | |||
| deb7c0d0c0 | |||
| bdfe3f18fe | |||
| 384324c85a | |||
| cd71da9330 | |||
| bef0d6fbca | |||
| 297b2beef8 | |||
| 00dc16eff8 | |||
| 891ccec7df | |||
| b41f18692b | |||
| d3911d4ba8 | |||
| bb9ec40c18 | |||
| 0f534731a6 | |||
| 9f233104a3 | |||
| 01d3aa725f | |||
| 8dfcccad41 | |||
| 4b533b5ff6 | |||
| 0e2d2a6fab | |||
| 3311269b23 | |||
| 8ca18725b6 | |||
| 2bed060d13 | |||
| 14140e26cd | |||
| 34664e867f | |||
| 082fd26bf1 | |||
| c8c230449e | |||
| 7f973f9a4f | |||
| 58f0dbbc84 | |||
| f4e121ae94 | |||
| f5834f8f63 | |||
| eb25f24c96 | |||
| 3f32bfda7c | |||
| 07a6c35470 | |||
| dec64e8a59 | |||
| 5d0f83b81c | |||
| a3b2da86b5 | |||
| 2d1d06899e | |||
| 1e2a6bab5b | |||
| b35409254e | |||
| 67b7a67f4b | |||
| f2dd27f443 | |||
| c6ef4f51f2 | |||
| dd55945670 | |||
| 03a8ee443d | |||
| a52fbc01a9 | |||
| 76b1bd20ae | |||
| e9de2b6f27 | |||
| f8358d117a | |||
| 9ca357e109 | |||
| 467c3f1bdd | |||
| 1ea8f67662 | |||
| 665b689410 | |||
| df6ea046c3 | |||
| 7fdd6582be | |||
| 84155274c4 | |||
| 21b828fe5c | |||
| 1cf132cb87 | |||
| 045ab0cdf6 | |||
| edb33400b3 | |||
| 691964877b | |||
| a8d54e9878 | |||
| ae044685e3 | |||
| 4499cedcb8 | |||
| 6cdef20cc2 | |||
| f74738a4e0 | |||
| 78a70765af | |||
| d1bea7f10e | |||
| ae5df73e72 | |||
| fc0f3d3790 | |||
| c736fd3b09 | |||
| 8e113f749a | |||
| e8dc823009 | |||
| 62a703974d | |||
| e235ec11f0 | |||
| 33dc9b6a18 | |||
| 364888bcc9 | |||
| 5cd53d4024 | |||
| 22c104f766 | |||
| 254097657e | |||
| b17f1d7401 | |||
| 404ee2ee3b | |||
| f8fe157352 | |||
| 61a6f3c827 | |||
| 0141f99dd1 | |||
| 67474a5cfe | |||
| 7e9fa4473c | |||
| ca69a3f9ae | |||
| 317d63d822 | |||
| 36358e7a43 | |||
| 968030535e | |||
| 6e132d25c8 | |||
| b4774a46ee | |||
| e6729b3123 | |||
| d6b3242b25 | |||
| 8a4f3f5ad6 | |||
| 9a56ee9b9d | |||
| 22d1e2d66a | |||
| 1adce5b58b | |||
| 328dee77c8 | |||
| 174a32c285 | |||
| 47e32eacd1 | |||
| d9c574f107 | |||
| 93e6954fb1 | |||
| 32050879a1 | |||
| 9660587e10 | |||
| 5ec13a073f | |||
| db3fa68052 | |||
| 423ea12856 | |||
| 9b482d609a | |||
| be06477907 | |||
| 08a6fc8a3f | |||
| 65b51c251e | |||
| 1af62d0879 | |||
| 5d71892b7c | |||
| 0c3c0c5e6a | |||
| 2fe4018d28 | |||
| 96423634ad | |||
| 3d7b12c98c | |||
| de85f201ad | |||
| a465984c52 | |||
| 0068411946 | |||
| ff658633fb | |||
| 8fbd7c7e60 | |||
| 4c4d225c03 | |||
| 0ec39f8e4e | |||
| 866037a0d7 | |||
| 50f255eea4 | |||
| e807fd556c | |||
| a26cc2eea7 | |||
| 3915af34c2 | |||
| 706c7abc55 | |||
| 1447304dd7 | |||
| 5f2900ffda | |||
| 0be4230487 | |||
| f294484c02 | |||
| 1675eb451f | |||
| 28a529088f | |||
| 292672a019 | |||
| 7b864a3201 | |||
| fffedf71d0 | |||
| e8a34255ac | |||
| 93bc8a90db | |||
| fe2d2bacd7 | |||
| f1049953c4 | |||
| 4d8526ad7e | |||
| 3e3c6088fb | |||
| 862b048b17 | |||
| 1a1110526e | |||
| ea5eedb5a5 | |||
| 8cc4bdf66b | |||
| 7a637967fe | |||
| c1dc475438 | |||
| 6c663cc87b | |||
| 9425bdc4e5 | |||
| d7b449869e | |||
| fa119d234b | |||
| b841efe4ba | |||
| 7b0deda1a6 | |||
| bc66326385 | |||
| 074189b73e | |||
| d83eabfdf1 | |||
| 25f4d49e43 | |||
| 95f2087148 | |||
| 9ec1b3a9bf | |||
| 6ee496ab1c | |||
| 9a4137e9da | |||
| 24c16ebee4 | |||
| 074f10f302 | |||
| 81b6782c9d | |||
| 521efe67db | |||
| c2a5df4be8 | |||
| 429eeb7754 | |||
| 08ac2f1125 | |||
| c2902a2246 | |||
| edaec666d1 | |||
| d8fa2ce6b0 | |||
| bcdcc8fe8c | |||
| bd998748ee | |||
| a071196f89 | |||
| b1fff7c921 | |||
| 231715bf98 | |||
| 5d9cbddb72 | |||
| 51a5e06f5c | |||
| 74006849b5 | |||
| 46888b6716 | |||
| 1d40f6d95a | |||
| d9b20f6eec | |||
| e634177ca4 | |||
| 70a92a8862 | |||
| 6dd1810038 | |||
| 978f128755 | |||
| d1c9359e96 | |||
| 83200a453b | |||
| c33522d1c7 | |||
| f048ffb6ac | |||
| 8fbb1f7d03 | |||
| cfb7fa5ca9 | |||
| e2626a0687 | |||
| c97e64c3c0 | |||
| 8808dda605 | |||
| 80764f85c1 | |||
| b6c4d703a2 | |||
| b9d0cca281 | |||
| c74099f5f8 | |||
| 8384ad3215 | |||
| e227b75472 | |||
| f65eda8b47 | |||
| 297578705e | |||
| ab84a5f936 | |||
| 61f802e9ad | |||
| f1153f59a2 | |||
| 0c6235e55b | |||
| 1c7ce84881 | |||
| 9539f7bcec | |||
| 649a65d854 | |||
| 61a6ab2e7b | |||
| 50aa535f88 | |||
| f3fec88888 | |||
| 5e46afdb0f | |||
| 7834b26ef8 | |||
| de9c92b360 | |||
| 6ccbed923e | |||
| ee0768bf3e | |||
| 7ceefa8c50 | |||
| 4add35b4ca | |||
| 045bf22e10 | |||
| 52270ab5d1 | |||
| 537d3a0ab3 | |||
| 4a285903e1 | |||
| d4b6979426 | |||
| 6e465b233f | |||
| f0485a20a8 | |||
| 6e90e178ed | |||
| 950d3134f2 | |||
| 5ac3d488a4 | |||
| ab34f8229b | |||
| f802bdd3ec | |||
| cc384efee0 | |||
| f815f3e3cf | |||
| 4e2087d6eb | |||
| e6893f58be | |||
| 9b8606483d | |||
| 2c578c6f1c | |||
| e397e4eee9 | |||
| dd86dce171 | |||
| e3813b1ea4 | |||
| bb2a4c10af | |||
| 1fc01df39c | |||
| 0e3cef813f | |||
| 87259f989d | |||
| 67d8079c47 | |||
| b247603b7e | |||
| f516148e46 | |||
| 5c50755652 | |||
| 3fae64f975 | |||
| 510360b002 | |||
| 9e4f4edb92 | |||
| ac815bf638 | |||
| 63c30b9ebe | |||
| 53ab87dc36 | |||
| 56e4b6d688 | |||
| 38d4ea72b1 | |||
| 9862ec82e3 | |||
| caa0a15803 | |||
| e909ec122c | |||
| d2cd775309 | |||
| c7a18fd4f0 | |||
| b44b88e00e | |||
| 54f5efb23f | |||
| e53f3197ac | |||
| d4fc9bbb91 | |||
| 28c54e4b39 | |||
| 1c814464c5 | |||
| 204ec93c5a | |||
| 72dbdca1f3 | |||
| 5d3a799e33 | |||
| 40a91f39ed | |||
| 5df0747642 | |||
| c64f8750e2 | |||
| dfcc851d2a | |||
| 259b4d9b35 | |||
| d46f0adb5e | |||
| eb1e780733 | |||
| 2eefae0618 | |||
| c35481f344 | |||
| 71735b10a2 | |||
| 8cf4b3fed6 | |||
| 2fdefa6040 | |||
| 55c1de8734 | |||
| 808c4a6f7a | |||
| 3a87d02696 | |||
| ce80e64b24 | |||
| c167ecc714 | |||
| 02448e176c | |||
| 7b17b4a1b9 | |||
| d70b00f30d | |||
| a476ec7976 | |||
| 19fcf60599 | |||
| 5ffe50ed02 | |||
| 3eda72e8c2 | |||
| d490d4f9f1 | |||
| e7f8ee6b91 | |||
| 4919c9c2fc | |||
| 253d8d3632 | |||
| 07505e7ef2 | |||
| 1e8b5b0523 | |||
| 63a1fa1378 | |||
| 8892f51096 | |||
| 7ef6c7ec75 | |||
| 7b8c134c21 | |||
| e3219f17dd | |||
| e738741521 | |||
| 69352babfd | |||
| 029a1ebba2 | |||
| 73a376ac6c | |||
| fe9d28bf4e | |||
| 1005a677f2 | |||
| dc71fc986c | |||
| 24a35f1174 | |||
| 473552cc81 | |||
| 6e2212de7b | |||
| fdf9866e88 | |||
| 47ba3d9028 | |||
| b24c523fa4 | |||
| 9e713bc39c | |||
| c446c07a4c | |||
| ab5584f421 | |||
| afa4d23a4f | |||
| b401f7acc1 | |||
| cc930a114f | |||
| 79ed7a335b | |||
| 12172d7b75 | |||
| 411206e45a | |||
| e146c7a337 | |||
| a5f52c4581 | |||
| a09b622a1d | |||
| c9dc592dcc | |||
| 80f3468ff8 | |||
| 12cfcb10ac | |||
| cc9cec922a | |||
| 7f8a7ea7ed | |||
| 0af0ef681f | |||
| cf42214974 | |||
| 31e6740033 | |||
| 4318adc98c | |||
| 3514d46954 | |||
| 63e29f024c | |||
| f4062010e9 | |||
| e8c1d54a96 | |||
| cac8c740fa | |||
| 73129b59d5 | |||
| 31d842fca4 | |||
| f5e7fe4245 | |||
| bf73f7edaf | |||
| 093ca0d260 | |||
| a2012a5afe | |||
| 9faaa2094d | |||
| 8075892e0e | |||
| 6cfaaf0bf9 | |||
| 05b09b4c98 | |||
| b1ccaf08bd | |||
| d0ad364ad4 | |||
| 70d05bd504 | |||
| f4533331e7 | |||
| dd6a449921 | |||
| ec9a650e65 | |||
| 3d2a103dfb | |||
| ae61e99e06 | |||
| 077b38e684 | |||
| d15cc79822 | |||
| 6bd3228990 | |||
| 7605a0253c | |||
| abf45ef9db | |||
| cf8b9e425e | |||
| 12f4ba8431 | |||
| 355201f094 | |||
| d163bad406 | |||
| 0aad2294ec | |||
| d2b0413c30 | |||
| a4c7a756f0 | |||
| 9f59000f5b | |||
| 7d8f34de17 | |||
| 9ad6347044 | |||
| 584e455f2d | |||
| 0a7525b125 | |||
| 1dad96102a | |||
| 20d011f8e9 | |||
| 637d07400e | |||
| 66a2bc2214 | |||
| 334469ef61 | |||
| e543525eb7 | |||
| 7e7f3ab297 | |||
| b0b0b62bce | |||
| 9259808f80 | |||
| 9662680bb0 | |||
| 6a24b14f0e | |||
| 57b66bdf47 | |||
| 0677aee93f | |||
| 0c73287e35 | |||
| dc75a5a5ab | |||
| c57eafcacc | |||
| f5ab5cf88a | |||
| 45e14dbb99 | |||
| cc89fc37a4 | |||
| 3ac0d8d5cb | |||
| e6f565a6c4 | |||
| 86492064aa | |||
| ba756cccc1 | |||
| 0998cb5a18 | |||
| f7f47152bd | |||
| 18d23ca20d | |||
| a88aecb993 | |||
| ab6f310013 | |||
| 4975c60ac1 | |||
| 5ca14eeaec | |||
| 031137a5fa | |||
| 6169cd05b9 | |||
| 9dff8f21bd | |||
| c5693db0e4 | |||
| ae52e9c4da | |||
| 182cbf3cc3 | |||
| b22f65f598 | |||
| b5d13180a6 | |||
| b12228c662 | |||
| d34fb5bc63 | |||
| f094acfdb5 | |||
| 4979cc0c94 | |||
| 9dfa2951f6 | |||
| 7f9f8de08a | |||
| 28f661c36f | |||
| 1256bbfec7 | |||
| bb267ff3d5 | |||
| 43d5eb5b88 | |||
| c1db0d71ed | |||
| 45fe909128 | |||
| 48c701cf1b | |||
| ebb1a00352 | |||
| 6a7f1dd393 | |||
| bd542bd6e4 | |||
| b055fc2496 | |||
| 1900f9ca80 | |||
| e010c2cecc | |||
| f92d7f89c6 | |||
| da57b20156 | |||
| 1a2259bdca | |||
| c0884705c7 | |||
| fa6c422f9b | |||
| 6dfdee75e2 | |||
| 2980f050ed | |||
| 132deac5bc | |||
| 9a43b14f80 | |||
| 619476ffed | |||
| 2a2f463d5b | |||
| ce94944c2d | |||
| 65c1b9ca5c | |||
| 95a65b05ca | |||
| d80786e99a | |||
| a868378b99 | |||
| 00ebd6df77 | |||
| 750d13779d | |||
| 6525c11b3c | |||
| 0224af64ab | |||
| 762a74624c | |||
| ac539b0150 | |||
| ad8f44b78a | |||
| fd750f3588 | |||
| 5b9403c343 | |||
| ef745a141d | |||
| 44ecd55a86 | |||
| eb1cd3e01a | |||
| cdfffa62bb | |||
| d8300a7bf9 | |||
| bda53f759e | |||
| 97d75e1d5e | |||
| 520575a839 | |||
| 989bdca736 | |||
| 3706920e16 | |||
| 00df8839e6 | |||
| dcf6c55a4e | |||
| f2b976c976 | |||
| b9eda646fc | |||
| 5b28063b52 | |||
| c1eb5f399f | |||
| 04cda22d19 | |||
| c10abd9f9f | |||
| f5d1a62ccf | |||
| 6942c36853 | |||
| 1b7408c6a1 | |||
| 66940f9c0a | |||
| a674c9b372 | |||
| 072f9a848e | |||
| 451bc9063c | |||
| 4fc4768083 | |||
| c660ac881c | |||
| eb428ba5f3 | |||
| 7d93f44e53 | |||
| 2c427816d9 | |||
| c55a1cc7f2 | |||
| ce3986bb02 | |||
| 447acc11bd | |||
| 51d82063aa | |||
| f97d54aa2a | |||
| 78c1f4f9ca | |||
| 4d3479d431 | |||
| 43c379aa0f | |||
| 3fb1b7cd87 | |||
| bd79b276e9 | |||
| 81d4344bdc | |||
| d9c3d1a602 | |||
| 54eeef5f8d | |||
| 208cad7ca1 | |||
| 043d8d81a2 | |||
| 28e8769a8b | |||
| dc9138846f | |||
| bfec11926f | |||
| 751532831a | |||
| e58e8ae263 | |||
| a90c58eb09 | |||
| d6ffdfa638 | |||
| 21bc611455 | |||
| 39a7593521 | |||
| 096f851408 | |||
| 7586c3ad25 | |||
| 762d39bdff | |||
| fb4d904ab6 | |||
| 40d78cbd92 | |||
| 55b0e2ebf6 | |||
| 885943e73f | |||
| 09247c57cf | |||
| 6117f73602 | |||
| 63d9f61bf1 | |||
| 3300fb3957 | |||
| 00d25dcc3e | |||
| 1bc255bc40 | |||
| cb05721a89 | |||
| 5801005fca | |||
| 572e09b2b1 | |||
| 3e76785b40 | |||
| 7a0c03dd21 | |||
| 1ce164a01e | |||
| a4a3d75cd9 | |||
| 70bda0c4df | |||
| 219e638316 | |||
| b16b7aaf0b | |||
| 38b6b81cd7 | |||
| 925d133466 | |||
| 6bcb5f4ae1 | |||
| c069459343 | |||
| cf814d4a97 | |||
| 841ae1d442 | |||
| 3137df4fe6 | |||
| ee2595e139 | |||
| 3c27606747 | |||
| 39ad8e85d9 | |||
| cbf38fb316 | |||
| d80226f948 | |||
| be74b4de6f | |||
| ce5bf0fe0e | |||
| 9ba093bf07 | |||
| 9c00cd3e44 | |||
| 6043a67631 | |||
| 2542cb591d | |||
| 9d1df6ec32 | |||
| f25289db20 | |||
| 470971bf70 | |||
| 6588c9d4ae | |||
| b839304f91 | |||
| b24f0dd572 | |||
| e175619543 | |||
| f369fbd227 | |||
| 4aaf27c669 | |||
| bfa763a55b | |||
| a916fa5118 | |||
| 44e752b606 | |||
| 29fcef3b9c | |||
| 2caf74946f | |||
| aca263642d | |||
| 005cc39394 | |||
| 7e07573026 | |||
| c9ee303b69 | |||
| 80da7e6275 | |||
| f81ca5c3c1 | |||
| 266e8cdd86 | |||
| 0a3df45d20 | |||
| 60a1547124 | |||
| 4b81b55849 | |||
| d41de1f7c9 | |||
| 5c4c10a1d7 | |||
| 2f3833a295 | |||
| 583783449a | |||
| bf36bc8a8f | |||
| e3e1bc7784 | |||
| 8fed9add66 | |||
| 647dfec334 | |||
| ad548840c7 | |||
| e9c2c51bc3 | |||
| b2a6a45a56 | |||
| 347352cc4c | |||
| f2cb3cd7e8 | |||
| 23e232e380 | |||
| 3eb5447f74 | |||
| f3f1336882 | |||
| ab4546e365 | |||
| ad3be0c53c | |||
| 0497029dae | |||
| bdf1a97550 | |||
| 57b8747c31 | |||
| d9287d6b1d | |||
| bccd26fb29 | |||
| 6a83c28e05 | |||
| 5af5bdd060 | |||
| 7f55554ae2 | |||
| dbb4ca6403 | |||
| 46c275c4c1 | |||
| 9ce69d23e3 | |||
| 02a8bcc324 | |||
| dd5cb5817b | |||
| ed385efa7b | |||
| 764a0296ce | |||
| 78a5719fae | |||
| b8c71d2a8e | |||
| 58e1105f2d | |||
| 0cb57774d7 | |||
| bc301c8d17 | |||
| 071c268de7 | |||
| 79776d1f6a | |||
| 0b149a6876 | |||
| 6c6dfae235 | |||
| 7cbd5d175c | |||
| 075a7b1430 | |||
| e1537d2d45 | |||
| 035e6dfe41 | |||
| 7c868585de | |||
| 2c44bae496 | |||
| 237e13c95e | |||
| 50ecb8472f | |||
| 872f458cb2 | |||
| 7c91d24595 | |||
| 790eda6f73 | |||
| 5dc8394f22 | |||
| 539f258d92 | |||
| bd6b12d2ea | |||
| 2c9f9ac549 | |||
| 3df6640fa5 | |||
| 4eef5ebbce | |||
| d6ca320269 | |||
| 53bb441f23 | |||
| 1f5e3c1c1a | |||
| 382826889f | |||
| 377b6d1186 | |||
| 3679ce1797 | |||
| b0143337d9 | |||
| 9452557f3c | |||
| 0d1b09e4f0 | |||
| 498593311f | |||
| c7c8e2779c | |||
| ea2c6ab246 | |||
| a71279a7b9 | |||
| 31cfbc2465 | |||
| 12f2dbe958 | |||
| b25dc328a2 | |||
| e4d1e95dcb | |||
| 07e5a20c0e | |||
| b798e3024e | |||
| 9ed0070039 | |||
| 109d8c5b4f | |||
| f5e9b5d6c2 | |||
| e65e862244 | |||
| c040fff8c8 | |||
| 23726afa90 | |||
| f55216af50 |
2858 changed files with 844809 additions and 60868 deletions
|
|
@ -6,6 +6,12 @@
|
||||||
"runtimeExecutable": "python3",
|
"runtimeExecutable": "python3",
|
||||||
"runtimeArgs": ["-m", "http.server", "8123", "-d", "build/web"],
|
"runtimeArgs": ["-m", "http.server", "8123", "-d", "build/web"],
|
||||||
"port": 8123
|
"port": 8123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ludic-docs",
|
||||||
|
"runtimeExecutable": "python3",
|
||||||
|
"runtimeArgs": ["-m", "http.server", "8124", "-d", "build/pages"],
|
||||||
|
"port": 8124
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ labels:
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
- Command used (e.g. `bin/x app foo.ludic --headless`):
|
- Command used (e.g. `bin/ludic build foo.ludic --headless`):
|
||||||
- Target (native macOS / headless / web-wasm):
|
- Target (native macOS / headless / web-wasm):
|
||||||
- Commit (`git rev-parse --short HEAD`):
|
- Commit (`git rev-parse --short HEAD`):
|
||||||
- OS / arch:
|
- OS / arch:
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ Closes #
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] `bin/x test` passes.
|
- [ ] `bin/ludic-dev test` passes.
|
||||||
- [ ] For compiler/runtime changes: `bin/x reseed && bin/x bootstrap-cfree`
|
- [ ] For compiler/runtime changes: `bin/ludic-dev reseed && bin/ludic-dev bootstrap-cfree`
|
||||||
still reaches the self-hosting fixpoint with no C compiler in the loop.
|
still reaches the self-hosting fixpoint with no C compiler in the loop.
|
||||||
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
|
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
|
||||||
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
|
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
|
||||||
registered in `tools/docgen/inventory.json`
|
registered in `tools/docgen/inventory.json`
|
||||||
(`python3 tools/docgen/check.py` passes).
|
(`bin/ludic-dev docs-gen && bin/ludic-dev docs-check build/pages` passes).
|
||||||
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
|
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
|
||||||
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
|
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
|
||||||
artifacts committed outside `build/` / `bin/`.
|
artifacts committed outside `build/` / `bin/`.
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,17 @@ jobs:
|
||||||
clang-16 --version | head -1
|
clang-16 --version | head -1
|
||||||
|
|
||||||
- name: Check out the triggering commit
|
- name: Check out the triggering commit
|
||||||
|
env:
|
||||||
|
# the repository that triggered the run, so a fork or a mirror tests itself
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
git clone https://git.workshopsoft.io/workshopsoft/ludic.git .
|
git clone "$REPO_URL" .
|
||||||
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
||||||
git log --oneline -1
|
git log --oneline -1
|
||||||
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
|
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
|
||||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
|
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||||
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Bootstrap x from the seed
|
- name: Bootstrap x from the seed
|
||||||
|
|
@ -40,11 +43,11 @@ jobs:
|
||||||
set -eu
|
set -eu
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
bin/ludicc tools/x/main.ludic -o bin/x
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
|
|
||||||
- name: Rebuild the compiler from the seed and assert byte-identity
|
- name: Rebuild the compiler from the seed and assert byte-identity
|
||||||
# `x bootstrap-cfree` assembles the seed with clang, has that seed
|
# `ludic-dev bootstrap-cfree` assembles the seed with clang, has that seed
|
||||||
# compiler recompile selfhost.ludic to out.ll, and `cmp`s out.ll against
|
# compiler recompile selfhost.ludic to out.ll, and `cmp`s out.ll against
|
||||||
# the checked-in seed. It returns non-zero if they differ — i.e. if the
|
# the checked-in seed. It returns non-zero if they differ — i.e. if the
|
||||||
# seed is stale relative to the compiler source.
|
# seed is stale relative to the compiler source.
|
||||||
run: bin/x bootstrap-cfree
|
run: bin/ludic-dev bootstrap-cfree
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ name: ci
|
||||||
|
|
||||||
# Build the language toolchain from its IR seed and run the regression suites on
|
# Build the language toolchain from its IR seed and run the regression suites on
|
||||||
# every push to main and every pull request. Until this landed the only workflow
|
# every push to main and every pull request. Until this landed the only workflow
|
||||||
# was docs.yml, so nothing gated a change on `x test` / `x test-tools` or on the
|
# was docs.yml, so nothing gated a change on `ludic-dev test` / `ludic-dev test-tools` or on the
|
||||||
# compiler even building from the seed. See also bootstrap.yml, which proves the
|
# compiler even building from the seed. See also bootstrap.yml, which proves the
|
||||||
# C-free self-rebuild reproduces the seed byte-for-byte.
|
# C-free self-rebuild reproduces the seed byte-for-byte.
|
||||||
on:
|
on:
|
||||||
|
|
@ -17,35 +17,38 @@ jobs:
|
||||||
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
|
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
# Reuse the runner's own base image (Debian bookworm with git + node already
|
# Reuse the runner's own base image (Debian bookworm with git + node already
|
||||||
# present) and add just the two things the toolchain needs: a modern clang
|
# present) and add just the one thing the toolchain needs: a modern clang
|
||||||
# (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required) and
|
# (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required). The docs
|
||||||
# python3 for the docs/vocabulary checks. A prebuilt image with these baked
|
# generator and its guards are now Ludic, so the job carries no Python. A
|
||||||
# in is the obvious future speed-up (see issue #33's packaging work).
|
# prebuilt image with clang baked in is the obvious future speed-up (see
|
||||||
|
# issue #33's packaging work).
|
||||||
container: node:20-bookworm
|
container: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
- name: Install clang-16 and python3
|
- name: Install clang-16
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -y -qq --no-install-recommends clang-16 python3 git ca-certificates
|
apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
|
||||||
clang-16 --version | head -1
|
clang-16 --version | head -1
|
||||||
python3 --version
|
|
||||||
|
|
||||||
- name: Check out the triggering commit
|
- name: Check out the triggering commit
|
||||||
|
env:
|
||||||
|
# the repository that triggered the run, so a fork or a mirror tests itself
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
git clone https://git.workshopsoft.io/workshopsoft/ludic.git .
|
git clone "$REPO_URL" .
|
||||||
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
||||||
git log --oneline -1
|
git log --oneline -1
|
||||||
# The toolchain is macOS-first; on this Linux runner it links against a
|
# The toolchain is macOS-first; on this Linux runner it links against a
|
||||||
# tiny C-free IR shim that supplies the Darwin standard-stream globals
|
# tiny C-free IR shim that supplies the Darwin standard-stream globals
|
||||||
# (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through
|
# (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through
|
||||||
# LUDIC_CC so every clang invocation — the seed bootstrap, `x build`,
|
# LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`,
|
||||||
# and each compiled test program — picks it up. Absolute path so it
|
# and each compiled test program — picks it up. Absolute path so it
|
||||||
# still resolves if a step changes directory.
|
# still resolves if a step changes directory.
|
||||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
|
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||||
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Bootstrap the toolchain from the IR seed (clang only)
|
- name: Bootstrap the toolchain from the IR seed (clang only)
|
||||||
|
|
@ -57,24 +60,30 @@ jobs:
|
||||||
# pre-built binaries: the language builds itself from source + seed.
|
# pre-built binaries: the language builds itself from source + seed.
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
bin/ludicc tools/x/main.ludic -o bin/x
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
bin/x build
|
bin/ludic-dev build
|
||||||
|
|
||||||
- name: Regression suite (x test)
|
- name: Regression suite (ludic-dev test)
|
||||||
run: bin/x test
|
run: bin/ludic-dev test
|
||||||
|
|
||||||
- name: Editor-toolchain suite (x test-tools)
|
- name: Editor-toolchain suite (ludic-dev test-tools)
|
||||||
# Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's
|
# Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's
|
||||||
# formatting contract — hand alignment is deliberately preserved, so the
|
# formatting contract — hand alignment is deliberately preserved, so the
|
||||||
# gate is fmt(fmt(x)) == fmt(x), not fmt(x) == x), and the JSON/XML editor
|
# gate is fmt(fmt(x)) == fmt(x), not fmt(x) == x), and the JSON/XML editor
|
||||||
# assets. Cross-file LSP behaviour and the golden renders are macOS-ABI
|
# assets. Cross-file LSP behaviour and the golden renders are macOS-ABI
|
||||||
# bound and skip here — visibly — until the runtime's directory walk and
|
# bound and skip here — visibly — until the runtime's directory walk and
|
||||||
# windowing are portable.
|
# windowing are portable.
|
||||||
run: bin/x test-tools
|
run: bin/ludic-dev test-tools
|
||||||
|
|
||||||
- name: Docs cover the implementation
|
- name: Docs cover the implementation
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
python3 tools/docgen/gen.py --out build/pages
|
# The whole docs toolchain is written in Ludic and runs through x —
|
||||||
python3 tools/docgen/check.py build/pages
|
# no Python anywhere. check-impl / check-vocabulary / check-docs guard
|
||||||
python3 tools/docgen/check-impl.py
|
# the sources; docs-gen builds the site and docs-check is its coverage
|
||||||
|
# + integrity guard. (check-vocabulary also runs in `ludic-dev test-tools`.)
|
||||||
|
bin/ludic-dev check-impl
|
||||||
|
bin/ludic-dev check-vocabulary
|
||||||
|
bin/ludic-dev check-docs
|
||||||
|
bin/ludic-dev docs-gen --out build/pages
|
||||||
|
bin/ludic-dev docs-check build/pages
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out with history
|
- name: Check out with history
|
||||||
env:
|
env:
|
||||||
BEFORE: ${{ github.event.before }}
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
BASE: ${{ github.base_ref }}
|
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
# Full clone so both endpoints of the range are present.
|
# Full clone so both endpoints of the range are present.
|
||||||
git clone https://git.workshopsoft.io/workshopsoft/ludic.git .
|
git clone "$REPO_URL" .
|
||||||
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
||||||
|
|
||||||
- name: Lint the new commits
|
- name: Lint the new commits
|
||||||
|
|
@ -36,10 +35,15 @@ jobs:
|
||||||
# - pull_request: base branch .. this commit
|
# - pull_request: base branch .. this commit
|
||||||
# - push: the pushed range (event.before .. this commit)
|
# - push: the pushed range (event.before .. this commit)
|
||||||
# - new branch / unknown: just the tip commit
|
# - new branch / unknown: just the tip commit
|
||||||
|
# `event.before` is only usable if it still resolves: a force-push
|
||||||
|
# rewrites (and a gc can remove) the commit it names, which made this
|
||||||
|
# job fail with "Invalid revision range" on an otherwise clean push.
|
||||||
|
# Fall back to the tip commit in that case.
|
||||||
if [ -n "${BASE:-}" ]; then
|
if [ -n "${BASE:-}" ]; then
|
||||||
git fetch --quiet origin "${BASE}" 2>/dev/null || true
|
git fetch --quiet origin "${BASE}" 2>/dev/null || true
|
||||||
RANGE="origin/${BASE}..${GITHUB_SHA}"
|
RANGE="origin/${BASE}..${GITHUB_SHA}"
|
||||||
elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$'; then
|
elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$' \
|
||||||
|
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
||||||
RANGE="${BEFORE}..${GITHUB_SHA}"
|
RANGE="${BEFORE}..${GITHUB_SHA}"
|
||||||
else
|
else
|
||||||
RANGE="${GITHUB_SHA}~1..${GITHUB_SHA}"
|
RANGE="${GITHUB_SHA}~1..${GITHUB_SHA}"
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,22 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'tools/docgen/**'
|
- 'tools/docgen/**'
|
||||||
|
- 'tools/ludic-cli/**'
|
||||||
|
# the site publishes the installer, so a change to it has to redeploy the
|
||||||
|
# site — otherwise a fixed install.sh sits in main while the old one is
|
||||||
|
# still what `curl … | sh` fetches
|
||||||
|
- 'install.sh'
|
||||||
- '.forgejo/workflows/docs.yml'
|
- '.forgejo/workflows/docs.yml'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
# Deploying is a force-push of an orphan branch, so two runs racing can land out
|
||||||
|
# of order and leave `pages` holding the older build — the site would silently
|
||||||
|
# go backwards with both runs green. Serialise them, and let a newer push cancel
|
||||||
|
# an older one that is still building rather than queue behind it.
|
||||||
|
concurrency:
|
||||||
|
group: pages-deploy
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
|
@ -23,22 +36,41 @@ jobs:
|
||||||
# GitHub-ism this runner does not register, so a job requesting it sits in
|
# GitHub-ism this runner does not register, so a job requesting it sits in
|
||||||
# "Waiting" forever with "no online runner found matching this label".
|
# "Waiting" forever with "no online runner found matching this label".
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
# Run in a Python image: the generator is pure-Python stdlib, and this image
|
# The generator is now Ludic, so this builds the toolchain from its IR seed
|
||||||
# already has git for the clone + publish. No node actions are used, so the
|
# (clang assembles the seed into bin/ludicc, which compiles bin/ludic) exactly
|
||||||
# job never depends on the runner's base image having python installed.
|
# like the ci workflow, then runs `ludic-dev docs-gen`. node:20-bookworm carries git
|
||||||
container: python:3.12
|
# for the clone + publish; clang-16 is the only extra the bootstrap needs.
|
||||||
|
container: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install clang-16
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
|
||||||
|
clang-16 --version | head -1
|
||||||
|
|
||||||
- name: Generate the documentation site
|
- name: Generate the documentation site
|
||||||
env:
|
env:
|
||||||
SOURCE_REF: ${{ github.ref_name }}
|
SOURCE_REF: ${{ github.ref_name }}
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
git clone --depth 1 --branch "${SOURCE_REF:-main}" \
|
git clone --depth 1 --branch "${SOURCE_REF:-main}" "$REPO_URL" src
|
||||||
https://git.workshopsoft.io/workshopsoft/ludic.git src
|
cd src
|
||||||
python3 --version
|
# The toolchain is macOS-first; on this Linux runner it links against a
|
||||||
python3 src/tools/docgen/gen.py --out public
|
# tiny C-free IR shim supplying the Darwin stdout/stderr globals over
|
||||||
python3 src/tools/docgen/check.py public
|
# glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no
|
||||||
|
# windowing), so the C-free bootstrap is all it needs.
|
||||||
|
export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm"
|
||||||
|
export LUDIC_HOME="$(pwd)"
|
||||||
|
mkdir -p bin
|
||||||
|
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
|
bin/ludic-dev docs-gen --out ../public
|
||||||
|
bin/ludic-dev docs-check ../public
|
||||||
|
cd ..
|
||||||
echo "--- generated files ---"
|
echo "--- generated files ---"
|
||||||
ls -la public
|
ls -la public
|
||||||
|
|
||||||
|
|
@ -47,6 +79,8 @@ jobs:
|
||||||
PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }}
|
PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }}
|
||||||
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SOURCE_SHA: ${{ github.sha }}
|
SOURCE_SHA: ${{ github.sha }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}"
|
TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}"
|
||||||
|
|
@ -60,5 +94,6 @@ jobs:
|
||||||
git config user.email "docs@workshopsoft.io"
|
git config user.email "docs@workshopsoft.io"
|
||||||
git add -A
|
git add -A
|
||||||
git commit -q -m "docs: regenerate site from ${SOURCE_SHA}"
|
git commit -q -m "docs: regenerate site from ${SOURCE_SHA}"
|
||||||
git push -f "https://ludic-docs-bot:${TOKEN}@git.workshopsoft.io/workshopsoft/ludic.git" pages
|
# the same server and repository the run came from, with the token spliced in
|
||||||
|
git push -f "${SERVER_URL%%://*}://ludic-docs-bot:${TOKEN}@${SERVER_URL#*://}/${REPO}.git" pages
|
||||||
echo "published $(git rev-parse --short HEAD) to pages"
|
echo "published $(git rev-parse --short HEAD) to pages"
|
||||||
|
|
|
||||||
99
.forgejo/workflows/release.yml
Normal file
99
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
# Cutting a release is `ludic-dev release` + `git push --tags`; everything after that
|
||||||
|
# happens here. Before this workflow existed the artifacts were built on whatever
|
||||||
|
# machine the maintainer happened to be sitting at, from whatever was in bin/ at
|
||||||
|
# the time, with no checksums and nothing proving the tagged tree even passed its
|
||||||
|
# tests. Now the tag is the trigger and CI is the only thing that publishes.
|
||||||
|
#
|
||||||
|
# The job refuses to publish unless:
|
||||||
|
# * the tag matches the VERSION file in the tagged tree,
|
||||||
|
# * CHANGELOG.md has a section for that version (it becomes the release notes),
|
||||||
|
# * the toolchain builds from the IR seed and the whole suite passes,
|
||||||
|
# * the C-free bootstrap still reproduces the seed byte-for-byte.
|
||||||
|
#
|
||||||
|
# Needs a repository secret FORGEJO_TOKEN with write access to releases.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to publish (e.g. v0.4.0)'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: docker
|
||||||
|
container: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- name: Install clang-16
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates curl
|
||||||
|
clang-16 --version | head -1
|
||||||
|
|
||||||
|
- name: Check out the tag
|
||||||
|
env:
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
|
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
git config --global --add safe.directory '*'
|
||||||
|
# A full clone: `git archive` needs the tag object, and the tarball is
|
||||||
|
# built from the tag rather than from the working tree.
|
||||||
|
git clone "$REPO_URL" .
|
||||||
|
TAG="${INPUT_TAG:-${GITHUB_REF_NAME}}"
|
||||||
|
git checkout "$TAG"
|
||||||
|
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
||||||
|
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
|
||||||
|
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||||
|
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: The tag, VERSION and CHANGELOG must agree
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
VERSION="$(cat VERSION)"
|
||||||
|
if [ "$TAG" != "v${VERSION}" ]; then
|
||||||
|
echo "::error::tag ${TAG} does not match VERSION (${VERSION})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "^## v${VERSION} " CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md has no '## v${VERSION}' section to use as release notes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "publishing ${TAG}"
|
||||||
|
|
||||||
|
- name: Build the toolchain from the IR seed (clang only)
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
mkdir -p bin
|
||||||
|
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
|
bin/ludic-dev build
|
||||||
|
|
||||||
|
- name: The tagged tree must pass its own suites
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
bin/ludic-dev test
|
||||||
|
bin/ludic-dev test-tools
|
||||||
|
bin/ludic-dev bootstrap-cfree
|
||||||
|
|
||||||
|
- name: Publish the release
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
LUDIC_FORGEJO_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ -z "${FORGEJO_TOKEN:-}" ]; then
|
||||||
|
echo "::error::No FORGEJO_TOKEN secret; cannot create the release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# ludic-dev publish builds dist/ (source tarball from the tag, this host's
|
||||||
|
# toolchain, SHA256SUMS), takes the notes from the CHANGELOG section,
|
||||||
|
# and creates the release. Re-running it only adds missing assets, so
|
||||||
|
# a maintainer can afterwards attach the macOS toolchain from a Mac
|
||||||
|
# with the same command.
|
||||||
|
bin/ludic-dev publish "$TAG"
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
packages/*/lib/** filter=lfs diff=lfs merge=lfs -text
|
||||||
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
# Generated build tree: LLVM IR, objects, compiled apps, the headless render
|
# Generated build tree: LLVM IR, objects, compiled apps, the headless render
|
||||||
# (build/out.ppm) and the docs site all land under build/ (see `bin/x build` /
|
# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic-dev build` /
|
||||||
# `bin/x clean`). Root-anchored so a source dir named "build" elsewhere is never
|
# `bin/ludic clean`). Root-anchored so a source dir named "build" elsewhere is never
|
||||||
# accidentally ignored. Nothing is written to the repo root any more.
|
# accidentally ignored. Nothing is written to the repo root any more.
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
|
|
@ -9,16 +9,24 @@
|
||||||
# packaged plugin .zip are local-only build inputs/outputs.
|
# packaged plugin .zip are local-only build inputs/outputs.
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
# the toolchain binaries (ludicc, ludic, x, ludic-fmt, ludic-lsp) — all built
|
# the toolchain binaries (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp) — all built
|
||||||
# into bin/ by the one-line bootstrap + `bin/x build`; never checked in. The
|
# into bin/ by the one-line bootstrap + `bin/ludic-dev build`; never checked in. The
|
||||||
# only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
|
# only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
|
||||||
/bin/
|
/bin/
|
||||||
|
|
||||||
|
# package manager (issue #63): the per-project linked view into the global
|
||||||
|
# content-addressed store, and the optional hermetic copy from `ludic vendor`. Both
|
||||||
|
# are regenerated by `ludic get` / `ludic vendor` — package.ludic + package.lock.ludic
|
||||||
|
# are the tracked source of truth, so these stay out of the tree.
|
||||||
|
ludic_modules/
|
||||||
|
vendor/
|
||||||
|
|
||||||
# editor toolchain build artifacts
|
# editor toolchain build artifacts
|
||||||
tools/editors/vscode/node_modules/
|
tools/editors/vscode/node_modules/
|
||||||
tools/editors/vscode/*.vsix
|
tools/editors/vscode/*.vsix
|
||||||
tools/editors/jetbrains/.gradle/
|
tools/editors/jetbrains/.gradle/
|
||||||
tools/editors/jetbrains/build/
|
tools/editors/jetbrains/build/
|
||||||
|
tools/editors/jetbrains/.kotlin/
|
||||||
|
|
||||||
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
|
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
|
||||||
.intellijPlatform/
|
.intellijPlatform/
|
||||||
|
|
@ -31,3 +39,24 @@ tools/editors/jetbrains/build/
|
||||||
# Python bytecode cache from the docgen / release tooling
|
# Python bytecode cache from the docgen / release tooling
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# Release artifacts produced by `ludic-dev release`
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Build/release tarballs anywhere in the tree. `git -C <repo> archive -o foo.tgz`
|
||||||
|
# resolves -o relative to the repo, not the caller's directory, so a stray
|
||||||
|
# archive lands in the root and a blanket `git add -A` will commit it.
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# The CC0 Poly Haven downloads are fetched, not committed (`ludic-dev fetch-assets`
|
||||||
|
# reads the manifest that ships with the renderer, packages/ludic.render3d/assets.manifest,
|
||||||
|
# so a game outside this repository fetches the same set with `ludic assets`).
|
||||||
|
assets/polyhaven/hdri/
|
||||||
|
assets/polyhaven/textures/
|
||||||
|
assets/polyhaven/models/
|
||||||
|
# `ludic run` beside an example writes its binary into a build/ there
|
||||||
|
examples/**/build/
|
||||||
|
|
||||||
|
# a package native/build.sh writes its objects under the package (phase 15)
|
||||||
|
packages/*/build/
|
||||||
|
|
|
||||||
984
BOOTSTRAP.md
984
BOOTSTRAP.md
|
|
@ -1,984 +0,0 @@
|
||||||
# Bootstrapping Ludic in Ludic
|
|
||||||
|
|
||||||
**What it would take for Ludic to compile itself.**
|
|
||||||
|
|
||||||
Today `ludicc` is a C program: 2,508 lines across `compiler/ludicc.c`,
|
|
||||||
`compiler/native.c` and `compiler/driver.c`. Everything it produces is
|
|
||||||
Ludic-or-IR — the runtime a game calls is 2,501 lines of `.ludic`, and no C is
|
|
||||||
generated, compiled or linked in a build. The compiler is the last C in the
|
|
||||||
pipeline, and this document is about removing it.
|
|
||||||
|
|
||||||
Every claim about what the language can and cannot do below was **verified
|
|
||||||
against the built compiler**, not read off the docs. The probe programs are in
|
|
||||||
the appendix; each `✅`/`❌` is a real compile-and-run.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What "completely bootstrapped" means
|
|
||||||
|
|
||||||
Self-hosting is not one property. It is three independent axes, and they cost
|
|
||||||
wildly different amounts:
|
|
||||||
|
|
||||||
| Axis | Today | Target |
|
|
||||||
|---|---|---|
|
|
||||||
| **Compiler independence** — is the compiler written in the language? | ❌ 2,508 lines of C | `ludicc` written in Ludic, compiling itself to a fixpoint |
|
|
||||||
| **Runtime independence** — is the library the language ships written in the language? | ✅ **already done** — 2,501 lines of `.ludic` (gfx, PNG/DEFLATE, TrueType, UI) | keep |
|
|
||||||
| **Toolchain independence** — does a build need a foreign compiler? | ❌ `clang` assembles the IR and links | see §7 — three levels, only one is worth reaching |
|
|
||||||
|
|
||||||
The runtime axis is already won, and that is the unusual part. Most languages
|
|
||||||
self-host the compiler long before they stop leaning on a C standard library;
|
|
||||||
Ludic did it backwards. **The remaining work is concentrated in one axis.**
|
|
||||||
|
|
||||||
There is also a fourth, smaller thing: `runtime/native/cocoa.ll` (327 lines) and
|
|
||||||
`runtime/web/wasm.ll` are hand-written LLVM IR, not Ludic. §7.4 covers whether
|
|
||||||
that matters.
|
|
||||||
|
|
||||||
Running alongside all of this is a question the bootstrap forces rather than
|
|
||||||
raises: **what the syntax should finally be.** A self-hosted compiler is written
|
|
||||||
in the language it compiles, so the grammar wants to be settled *before* the
|
|
||||||
port, not after. §5 audits what is irregular today and proposes the freeze; it
|
|
||||||
is scheduled as Stage 0.5, between the language features and the libraries.
|
|
||||||
|
|
||||||
### The honest bar
|
|
||||||
|
|
||||||
"Bootstrapped by itself completely" should mean:
|
|
||||||
|
|
||||||
1. `ludicc` is written in Ludic.
|
|
||||||
2. A `ludicc` binary compiles the Ludic source of `ludicc` and produces a
|
|
||||||
**byte-identical** binary to itself (the fixpoint test, §6).
|
|
||||||
3. The C compiler is needed **only** to build the very first seed, and that seed
|
|
||||||
is a checked-in artifact rather than a live dependency.
|
|
||||||
4. No C source remains in the repo outside that seed.
|
|
||||||
|
|
||||||
It should *not* mean writing an object-file writer and a linker. Rust and Swift
|
|
||||||
are self-hosted and both stand on LLVM; standing on `clang` as an IR assembler
|
|
||||||
is the same posture. §7 argues this explicitly so the goal does not quietly
|
|
||||||
inflate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Where the tree stands
|
|
||||||
|
|
||||||
```
|
|
||||||
compiler/ C split by concern; every file under 500 lines
|
|
||||||
ludicc.c 435 pipeline + codegen glue + main
|
|
||||||
util/ sb, diag 103 string builder; source registry + diagnostics
|
|
||||||
front/ lex, ast, parse 469 tokens; Node; recursive descent + imports
|
|
||||||
sem/ tables, uitree, validate 208 decl tables; widget flattening; static checks
|
|
||||||
back/ ir_* x10 953 the LLVM IR backend, one file per concern
|
|
||||||
driver/ toolchain, webbundle 382 IR -> object -> exe/dylib; the wasm bundle
|
|
||||||
fmt/ fmt 162 canonical AST printer (--fmt)
|
|
||||||
------
|
|
||||||
2,712 C <- all of it, and all that must go
|
|
||||||
tools/ludic-tools/* 3,260 C ludic-fmt + ludic-lsp (not yet split)
|
|
||||||
|
|
||||||
runtime/native/core.ludic 394 Ludic framebuffer, text, registers, RNG, input
|
|
||||||
runtime/native/image.ludic 436 Ludic PNG, sprites, alpha blend, 9-slice
|
|
||||||
runtime/native/inflate.ludic 276 Ludic DEFLATE (RFC 1951)
|
|
||||||
runtime/native/truetype.ludic 804 Ludic sfnt loader + AA rasterizer, Q16.16
|
|
||||||
runtime/native/ui.ludic 591 Ludic retained widget tree, layout, focus
|
|
||||||
----
|
|
||||||
2,501 Ludic <- proof the language is already load-bearing
|
|
||||||
|
|
||||||
runtime/native/cocoa.ll 327 LLVM IR macOS window (objc_msgSend + CoreGraphics)
|
|
||||||
runtime/web/wasm.ll 369 LLVM IR browser shims
|
|
||||||
```
|
|
||||||
|
|
||||||
`util/`, `front/`, `sem/` and `fmt/` are separately compiled translation units;
|
|
||||||
`back/` and `driver/` are still one unit assembled by `back/native.c`, so their
|
|
||||||
include order is their definition order. The build list lives in
|
|
||||||
`compiler/sources.sh`, sourced by both `build.sh` and `test.sh`.
|
|
||||||
|
|
||||||
`truetype.ludic` matters more than its line count. A from-scratch sfnt parser
|
|
||||||
with cmap format dispatch, composite glyph recursion and a Bézier rasterizer is
|
|
||||||
*structurally the same kind of program as a compiler*: binary input, recursive
|
|
||||||
descent, table lookups, a growing output buffer. It already works. That is the
|
|
||||||
strongest single piece of evidence that this port is feasible rather than
|
|
||||||
aspirational.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. What the language can already do
|
|
||||||
|
|
||||||
All verified. A compiler needs each of these, and each one works today.
|
|
||||||
|
|
||||||
| Capability | Status | Evidence |
|
|
||||||
|---|---|---|
|
|
||||||
| Recursion | ✅ | `fib(10)` → `55` |
|
|
||||||
| Mutual recursion / forward references | ✅ | `odd`/`even` cross-call |
|
|
||||||
| Deep recursion (recursive-descent parsing) | ✅ | 5,000 frames, no crash |
|
|
||||||
| Heap allocation | ✅ | `mem_alloc`, `mem_free`, `mem_copy`, `mem_set`; 1 MiB alloc verified |
|
|
||||||
| Byte-level memory | ✅ | `peek8`/`poke8`, `peek32`/`poke32`, `peekp`/`pokep`, `ptr_add` |
|
|
||||||
| `ptr` locals, params, returns | ✅ | `function make(n: int) -> pointer` |
|
|
||||||
| `ptr` in a property field | ✅ | `property Nd { kind: int = 0, a: pointer = ptr_null() }` |
|
|
||||||
| String literals as readable bytes | ✅ | `peek8("hello", 1)` → `101` |
|
|
||||||
| `str` accepted where `ptr` expected | ✅ | `f("A")` into `function f(p: pointer)` |
|
|
||||||
| String comparison, **hand-written in Ludic** | ✅ | `streq` over `peek8` |
|
|
||||||
| Integer → decimal, **hand-written in Ludic** | ✅ | `itoa(48291)` → `"48291"` |
|
|
||||||
| File read: open/seek/tell/read/close | ✅ | full round-trip of a written file |
|
|
||||||
| File write | ✅ | `file_open`/`file_write`/`file_close` |
|
|
||||||
| Module-level mutable state | ✅ | `var count: int`, `var heap: pointer` |
|
|
||||||
| `let` is mutable | ✅ | `i = i + 1` in a loop |
|
|
||||||
| `while`, numeric `for i in a .. b` with runtime bounds | ✅ | |
|
|
||||||
| `if` / `else if` / `else` chains | ✅ | |
|
|
||||||
| `match` with multi-value arms and `_` | ✅ | `1 => … 2, 3 => … _ => …` |
|
|
||||||
| Bitwise ops | ✅ | `band`/`bor`/`bxor`/`bnot`/`shl`/`shr` |
|
|
||||||
| `shr` is **logical**, not arithmetic | ✅ | `shr(-16, 1)` → `2147483640` |
|
|
||||||
| Character literals | ✅ | `'x'`, `'\n'`, `'\0'` lex to ints |
|
|
||||||
| Exit codes | ✅ | `os_exit(3)` → shell sees `3` |
|
|
||||||
| Separate compilation, C ABI | ✅ | `module` + `@export fn`, `extern fn … = "sym"` |
|
|
||||||
|
|
||||||
**The consequence:** a compiler is *already expressible* in Ludic today. You
|
|
||||||
could write a lexer, a parser building nodes as hand-offset `peek32`/`poke32`
|
|
||||||
records, a symbol table, and an IR text emitter, using nothing above. It would
|
|
||||||
be miserable to read and maintain at 6,000 lines — but nothing in §4 is a
|
|
||||||
*capability* blocker except argv. The rest is about whether the resulting source
|
|
||||||
is something a human or a model can work in.
|
|
||||||
|
|
||||||
That distinction shapes the whole plan: **this is mostly an ergonomics project
|
|
||||||
with one small hole in it**, not a language-design project.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. What the language is missing
|
|
||||||
|
|
||||||
Each entry: the gap, why a compiler specifically needs it, the proposed design,
|
|
||||||
and the lowering. Verified-missing means it is a compile error today.
|
|
||||||
|
|
||||||
### Tier A — real blockers
|
|
||||||
|
|
||||||
#### A1. Command-line arguments ❌ *the only true capability blocker*
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 1: unknown function 'os_argc'
|
|
||||||
```
|
|
||||||
|
|
||||||
`ll_emit_main` in `compiler/native.c` emits `define i32 @main()` — **no
|
|
||||||
parameters**. A self-hosted `ludicc` has no way to learn which file to compile.
|
|
||||||
Everything else in this document has a workaround; this one does not.
|
|
||||||
|
|
||||||
**Design.** Two intrinsics:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — proposed signature notation, not code
|
|
||||||
os_argc() -> int
|
|
||||||
os_arg(i: int) -> str
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lowering.** Change the signature to `define i32 @main(i32 %argc, ptr %argv)`,
|
|
||||||
store both into `@L_argc` / `@L_argv` in the entry block, then `os_argc()` is a
|
|
||||||
load and `os_arg(i)` is exactly the existing `peekp(@L_argv, i)` path. Add to
|
|
||||||
`INTRINSICS[]` in `native.c`.
|
|
||||||
|
|
||||||
**Cost.** ~30 lines of C. This is the single highest-value change in the
|
|
||||||
document: it is what turns "a Ludic program" into "a Ludic command-line tool".
|
|
||||||
|
|
||||||
#### A2. Aggregate types (`struct`) ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 2: expected declaration (got 'struct')
|
|
||||||
```
|
|
||||||
|
|
||||||
An AST node, a token, a symbol-table entry and a type descriptor are all
|
|
||||||
records. Today there are two workarounds, and both are bad at compiler scale:
|
|
||||||
|
|
||||||
- **Hand-offset memory** — `poke32(n, 0, kind)`, `pokep(n, 1, child)`. This is
|
|
||||||
what `truetype.ludic` does, and it works, but every field access becomes a
|
|
||||||
magic number. Across a 6,000-line compiler this is the difference between
|
|
||||||
maintainable and not.
|
|
||||||
- **ECS entities as nodes** — verified working (`property Nd { kind, a: pointer }`),
|
|
||||||
and initially seductive because queries give you free traversal. **Do not do
|
|
||||||
this.** `LUDIC_MAX_ENT` is 1024 in `native.c:18`; the entity world is a fixed
|
|
||||||
array of per-property storage. A compiler needs hundreds of thousands of
|
|
||||||
nodes. This is a dead end, and it is worth writing down because it is the
|
|
||||||
obvious wrong turn.
|
|
||||||
|
|
||||||
**Design — reference semantics, not value semantics.** The cheap version that
|
|
||||||
unblocks everything:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — proposed syntax: struct does not exist yet
|
|
||||||
struct Tok { kind: int = 0, text: pointer = ptr_null(), line: int = 0 }
|
|
||||||
|
|
||||||
let t = new Tok # heap-allocated, fields seeded from defaults
|
|
||||||
t.kind = T_ID
|
|
||||||
print_int(t.line)
|
|
||||||
free Tok t # or leak it; see §8 on arenas
|
|
||||||
```
|
|
||||||
|
|
||||||
No copying, no by-value passing, no nested-struct inlining — a `struct` value
|
|
||||||
*is* a `ptr` with a known layout, so it costs nothing in the type system beyond
|
|
||||||
a layout table.
|
|
||||||
|
|
||||||
**Lowering.** This is largely already built. `native.c` already emits
|
|
||||||
`%Cmp_<Name>` LLVM struct types for properties and already resolves
|
|
||||||
`a.b` through `ll_member_addr` with `getelementptr`. A `struct` is a
|
|
||||||
`%Cmp_`-style type *without* the parallel entity arrays: `new` is
|
|
||||||
`malloc(sizeof)` plus a default-seeding memset/store sequence, and `.field` is
|
|
||||||
the existing `getelementptr` path. Reusing the property machinery is why this
|
|
||||||
is far cheaper than it looks.
|
|
||||||
|
|
||||||
**Cost.** ~250 lines of C across `ludicc.c` (parse) and `native.c` (layout,
|
|
||||||
`new`, member access). Highest cost in the document, and the highest payoff.
|
|
||||||
|
|
||||||
#### A3. Arrays and indexing ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 2: expected identifier (got '[')
|
|
||||||
```
|
|
||||||
|
|
||||||
Token buffers, string tables, keyword tables, scope stacks. Currently
|
|
||||||
`mem_alloc` + `peek32`, which works but reads badly.
|
|
||||||
|
|
||||||
**Design.**
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — proposed syntax: array types do not exist yet
|
|
||||||
var keywords: [str; 64] # fixed-size module-level storage
|
|
||||||
let toks: [Tok; 0] = mem_alloc(n * size_of(Tok)) # or a growable buffer
|
|
||||||
toks[i].kind = T_ID # composes with A2
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lowering.** `[T; N]` is `[N x <llty(T)>]`, already exactly how `@L_alive` and
|
|
||||||
`@S_<Comp>` are emitted. `a[i]` as both rvalue and lvalue is a
|
|
||||||
`getelementptr` — the same code path as member access, indexed instead of
|
|
||||||
named. The important part is that `toks[i].kind` composes: index then member,
|
|
||||||
one GEP chain.
|
|
||||||
|
|
||||||
**Cost.** ~150 lines. Should land *with* A2, since neither is much use alone.
|
|
||||||
|
|
||||||
#### A4. `break` / `continue` ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 3: unknown identifier 'break'
|
|
||||||
```
|
|
||||||
|
|
||||||
Lexers and parsers are made of `while (1) { … break; }`. The workaround —
|
|
||||||
sentinel booleans threaded through every loop condition — is the kind of thing
|
|
||||||
that makes a 6,000-line port unreadable.
|
|
||||||
|
|
||||||
**Design.** `break`, `continue`. No labels; nested loops in a compiler rarely
|
|
||||||
need them, and adding labels later is compatible.
|
|
||||||
|
|
||||||
**Lowering.** `native.c` already maintains `ll_loopstk[64]` (for `self()` inside
|
|
||||||
queries). Extend each frame with `break_label` and `continue_label`, then
|
|
||||||
`break` is `br label %<break>`. Note the existing gotcha recorded in the native
|
|
||||||
backend notes: **stack slots must be emitted in the entry block** — no new
|
|
||||||
allocas at the break site.
|
|
||||||
|
|
||||||
**Cost.** ~40 lines. Best value-per-line in the document.
|
|
||||||
|
|
||||||
#### A5. `mem_realloc` ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 1: unknown function 'mem_realloc'
|
|
||||||
```
|
|
||||||
|
|
||||||
Every table in a compiler grows: tokens, nodes, the output buffer. Hand-rolling
|
|
||||||
alloc-copy-free works but is written once per table and gotten wrong once per
|
|
||||||
table.
|
|
||||||
|
|
||||||
**Design.** `mem_realloc(p: pointer, n: int) -> pointer`.
|
|
||||||
|
|
||||||
**Lowering.** `declare ptr @realloc(ptr, <size_t>)` plus one `INTRINSICS[]`
|
|
||||||
entry. **Use `ll_size_t()` / `ll_widen()` for the size argument — do not
|
|
||||||
hardcode `i64`.** `size_t` is `i32` on wasm32, and `native.c` now routes every
|
|
||||||
size-taking intrinsic through those helpers for exactly this reason.
|
|
||||||
|
|
||||||
**Cost.** ~6 lines.
|
|
||||||
|
|
||||||
#### A6. Diagnostics on stderr ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 1: unknown function 'print_err'
|
|
||||||
```
|
|
||||||
|
|
||||||
Only stdout exists (`print_str` → `printf`, `write_byte` → `putchar`). This is
|
|
||||||
not cosmetic: **`ludicc --emit llvm` writes IR to stdout.** A self-hosted
|
|
||||||
compiler that printed errors to stdout would interleave diagnostics into its own
|
|
||||||
output, corrupting it in exactly the case you most want a diagnostic.
|
|
||||||
|
|
||||||
**Design.** Prefer an intrinsic that yields a handle, so the existing file
|
|
||||||
plumbing is reused rather than duplicated:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — proposed signature notation, not code
|
|
||||||
file_stderr() -> pointer # then file_write(f, buf, n) as usual
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lowering — note the portability wrinkle.** There is no portable `@stderr`
|
|
||||||
global in LLVM IR: Darwin exports `@__stderrp`, glibc exports `@stderr`, and
|
|
||||||
wasm has neither in the same shape. So `file_stderr()` must select per target,
|
|
||||||
alongside the existing `target_os()` logic in `driver.c`. This is the one item
|
|
||||||
here that is genuinely target-dependent rather than merely unimplemented, and
|
|
||||||
it should be designed with that in mind rather than bolted on.
|
|
||||||
|
|
||||||
**Cost.** ~40 lines including the per-target selection.
|
|
||||||
|
|
||||||
### Tier B — needed for *complete* bootstrap, not for the compiler
|
|
||||||
|
|
||||||
#### B1. Function pointers ❌
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc: error: line 3: unknown type 'fn' for var h
|
|
||||||
```
|
|
||||||
`&cb` also fails to compile.
|
|
||||||
|
|
||||||
The compiler itself does **not** need these — `match` dispatch covers every
|
|
||||||
place a C compiler would use a function pointer table.
|
|
||||||
|
|
||||||
But they are what would let `cocoa.ll` become Ludic. The macOS window builds an
|
|
||||||
`NSView` subclass at runtime with `objc_allocateClassPair` and installs **an IR
|
|
||||||
function as its IMP**. Without the ability to take the address of a Ludic `fn`,
|
|
||||||
that shim can never move out of hand-written IR. So: irrelevant to §6, and
|
|
||||||
load-bearing for §7.4.
|
|
||||||
|
|
||||||
**Design.** `&fnname` yields a `ptr`; call through it via
|
|
||||||
`call_ptr(p, args…)` or a typed `fn(int)->int` type.
|
|
||||||
|
|
||||||
**Cost.** ~120 lines. Defer until after the fixpoint.
|
|
||||||
|
|
||||||
#### B2. String operations — **no language change needed**
|
|
||||||
|
|
||||||
`str + str` is worth calling out as a *bug*, not a gap. It passes the front-end
|
|
||||||
and then emits invalid IR:
|
|
||||||
|
|
||||||
```
|
|
||||||
build/probe_t_headless.ll:13401:17: error: global variable reference must have pointer type
|
|
||||||
```
|
|
||||||
|
|
||||||
That is a front-end/backend mismatch: the typechecker accepts an operation the
|
|
||||||
backend cannot lower. Until strings exist properly, `str + str` should be a
|
|
||||||
clean compile error rather than a `clang` error in generated code.
|
|
||||||
|
|
||||||
Everything else a compiler needs from strings is **already writable in Ludic
|
|
||||||
today** — `streq` and `itoa` are verified. This is not a language gap; it is a
|
|
||||||
library to write (§6 Stage 1), and it is the largest pure-typing chunk of the
|
|
||||||
whole project.
|
|
||||||
|
|
||||||
### Tier C — explicitly out of scope, recorded so they are not rediscovered
|
|
||||||
|
|
||||||
| Gap | Why it does not block |
|
|
||||||
|---|---|
|
|
||||||
| **64-bit integers** ❌ (`100000*100000` → `1410065408`, wraps at i32) | Line numbers, offsets, node indices and string lengths all fit in `i32`. Only matters for source files > 2 GiB. |
|
|
||||||
| **A non-ECS entry point** | A `Start`-phase system plus `os_exit(n)` gives correct exit codes — verified. You do pay for an unused 1024-entity world; that is a constant, not a blocker. A `tool Name { function main() -> int }` form would be nicer, not necessary. |
|
|
||||||
| Closures, generics, unions, sum types | A compiler in the style of `ludicc.c` uses none of them. |
|
|
||||||
| GC | A compiler should leak deliberately (§8). |
|
|
||||||
| Unsigned integer types | `shr` is already logical and `band`/`bor` are bit-level — sufficient. |
|
|
||||||
| Multiple return values | `ptr` out-parameters work today. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Designing for readers — human and model
|
|
||||||
|
|
||||||
The goal: Ludic source should be obvious to a person skimming it and
|
|
||||||
unambiguous to a model generating it. Those two goals agree far more than they
|
|
||||||
conflict, and where they conflict the resolution is **regularity, not
|
|
||||||
verbosity** (§5.2).
|
|
||||||
|
|
||||||
Everything in this section was verified against the built compiler. The probes
|
|
||||||
are in the appendix under "Syntax audit".
|
|
||||||
|
|
||||||
### 5.1 Why this belongs in the bootstrap document, and why now
|
|
||||||
|
|
||||||
**Syntax changes are cheap today and expensive after Stage 3.** This is a hard
|
|
||||||
ordering constraint, not a preference.
|
|
||||||
|
|
||||||
Today, changing the grammar costs: edit `ludicc.c`, `sed` three examples and
|
|
||||||
five runtime files, run `bin/x test`. An afternoon.
|
|
||||||
|
|
||||||
After the fixpoint, `ludicc` is *written in the syntax it parses*. Every change
|
|
||||||
becomes a four-step dance: build a compiler that accepts both old and new forms
|
|
||||||
→ compile it with the old seed → rewrite every source file → remove the old
|
|
||||||
form and regenerate the seed. That is what every mature language does, and it
|
|
||||||
is why mature languages change syntax slowly. It is not a reason to avoid the
|
|
||||||
change; it is a reason to **make it before the port, not after**.
|
|
||||||
|
|
||||||
So the plan gains a stage:
|
|
||||||
|
|
||||||
> **Stage 0.5 — Syntax freeze.** Between Stage 0 (language features) and
|
|
||||||
> Stage 1 (libraries). Nothing in Stage 2 starts until the grammar is final.
|
|
||||||
|
|
||||||
The port should be *the first large program written in final Ludic*, not the
|
|
||||||
last large program written in provisional Ludic.
|
|
||||||
|
|
||||||
**This work also strengthens the bootstrap itself.** Stage 2b uses `--fmt`
|
|
||||||
equality as the oracle proving two parsers agree. That oracle is only as tight
|
|
||||||
as the language is regular: every alternative spelling is surface variance the
|
|
||||||
formatter must erase. Reduce the variance and the oracle gets sharper. The
|
|
||||||
readability project and the self-hosting project are not competing for the same
|
|
||||||
time — one makes the other more trustworthy.
|
|
||||||
|
|
||||||
### 5.2 What actually helps a model — and what is folklore
|
|
||||||
|
|
||||||
Worth being precise here, because "AI-friendly syntax" attracts a lot of
|
|
||||||
confident nonsense.
|
|
||||||
|
|
||||||
**Genuinely helps:**
|
|
||||||
|
|
||||||
| Property | Why it matters |
|
|
||||||
|---|---|
|
|
||||||
| **Low syntactic variance** — one spelling per concept | Every alternative is a branch point during generation and a case in the parser. Two ways to write a list is two chances to be inconsistent within one file. |
|
|
||||||
| **Leading-keyword, bounded lookahead** | Every declaration and statement identifiable from its first token. Helps the hand-written recursive-descent parser Stage 2b will be, *and* a model predicting forward. |
|
|
||||||
| **No silent no-ops** | If the language accepts a construct it must either honour it or reject it. Accepting-and-ignoring teaches a falsehood (see R6 — the worst thing in the audit). |
|
|
||||||
| **Recoverable structure** — explicit terminators | A slightly-wrong generation fails *locally*, with an error pointing at the mistake, instead of cascading into a confusing error 40 lines later. |
|
|
||||||
| **Locality** — meaning readable from the construct | No action-at-a-distance. Ludic is already strong here; keep it. |
|
|
||||||
| **Greppable unique anchors** | `property Pos` is findable. Retrieval quality is a language design property. |
|
|
||||||
| **Errors that name the fix** | Already partly true: a missing builtin errors naming `rt_<name>`. Extend that everywhere. |
|
|
||||||
|
|
||||||
**Folklore, and false:**
|
|
||||||
|
|
||||||
- *"More verbose is more AI-friendly."* No. Ceremony without information hurts
|
|
||||||
both audiences. What helps is redundancy that **encodes intent** — an explicit
|
|
||||||
type, a closing keyword — not boilerplate.
|
|
||||||
- *"Significant indentation reads better."* It reads fine and **generates
|
|
||||||
badly**: indentation drift across a long generated block is unrecoverable and
|
|
||||||
survives review. Ludic uses braces. Keep them.
|
|
||||||
- *"Natural-language-like syntax helps."* Prose-shaped keywords add ambiguity.
|
|
||||||
Consistent symbols beat English words that read three ways.
|
|
||||||
- *"Terseness is bad for models."* Terseness is fine; *irregularity* is the
|
|
||||||
problem. A short form used consistently is easy to predict.
|
|
||||||
|
|
||||||
**The real tension:** humans skim, so terseness helps them; machines benefit
|
|
||||||
from redundancy. Regularity resolves it — the same shape everywhere costs a
|
|
||||||
human nothing once learned, and costs a model nothing to predict.
|
|
||||||
|
|
||||||
### 5.3 Audit — what is irregular in Ludic today
|
|
||||||
|
|
||||||
Each row verified by compiling a probe, not by reading docs.
|
|
||||||
|
|
||||||
| # | Irregularity | Evidence | Cost |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **R1** | **No statement terminator at all.** `block()` is `skipnl(); stmt()` in a loop. A newline *stops* an expression (it lexes as `T_NL`, and `binlevel` only continues on `T_OP`) but is never *required*. `let x = 1 x = x + 1 print_int(x)` on one line is three legal statements — verified compiling. | `ludicc.c` `block()`, `binlevel` | The reader cannot see where a statement ends without re-deriving operator precedence. Blocks error recovery entirely. |
|
|
||||||
| **R2** | **Commas are optional everywhere.** `if(isop(",")) pi++` appears in `comp()`, `arche()`, `fn` params and `spawn`. `{ x: int = 0 y: int = 0 }` and the comma'd form both compile. | 4 parser sites | Two spellings, zero semantic difference. |
|
|
||||||
| ~~**R3**~~ | ~~**`and`/`or` alias `&&`/`\|\|`.**~~ **RESOLVED** — `and`/`or`/`not` are the only boolean operators; `&&`, `\|\|` and `!` are each rejected with a diagnostic naming the fix, and all three words are reserved. `!=` is unaffected. | landed via S3 | — |
|
|
||||||
| **R4** | **`{ }` means seven different things** — statement block; property fields (`n: T = e`); model list (bare idents); spawn initialisers (`N = { … }`); ui props + children (`k=v` juxtaposed, no commas); match arms (`p, p => …`); machine states (`state N = v { … }`). | `block/comp/arche/spawn/parse_widget/match/machine` | The delimiter carries no information. You must already know the head keyword to know the inner grammar. |
|
|
||||||
| **R5** | **Contextual keywords, not reserved.** `phase`, `query`, `reads`, `writes`, `needs`, `uses`, `where`, `in`, `on`, `layer`, `state`, `start` are matched with `isid()` — ordinary identifiers. `let query = 5 let phase = 6` compiles and prints `11`. | `sys()`, `scene_decl()` | A local named `enter` or `match` produces a baffling error far from the cause. |
|
|
||||||
| **R6** | **Contracts are parsed and thrown away.** `requires`/`ensures`/`invariant` parse an expression and **discard it** (`pi++; expr();`). `reads`/`writes`/`needs`/`uses`/`effects` are `skip_brackets()`. `pure` is consumed and ignored. Verified: `function half(n: int) -> int requires n > 100000 ensures false` compiles, and `half(8)` returns `4`. Verified: a system declaring `reads [Pos]` that **writes** `p.x = 99` compiles. | `fn()`, `sys()` | **The worst item in the audit.** The language accepts a contract and does nothing. A model writing `requires n > 0` is rewarded with a clean compile and zero enforcement — it learns a lie, and so does a human reader trusting the annotation. |
|
|
||||||
| **R7** | **`str + str` typechecks, then emits invalid IR.** | verified (§4 B2) | The front-end accepts what the backend cannot lower. |
|
|
||||||
| **R8** | **Two formatters, opposite philosophies, both called "format".** `ludicc --fmt` canonicalises hard (one statement per line, `and`→`&&`, full parenthesisation) but drops comments and inlines imports. `ludic-fmt` is token-based and preserves comments — but **normalises nothing**: handed the one-line `let a = 1 a = a + 1 if true and false { … }`, it returned it unchanged. | verified side-by-side | **Neither tool enforces a single spelling.** The canonicaliser is unusable on real source; the source formatter has no opinion. |
|
|
||||||
| **R9** | **Two ways to spell a tag** — `property Player { }` (empty property) or `model`. | LANGUAGE.md | |
|
|
||||||
| **R10** | **Stale docs are stale training data.** LANGUAGE.md still says "the current compiler is a tree-to-C translator" (it emits LLVM IR) and lists arrays under "Not yet implemented" beside things never planned. | LANGUAGE.md | Docs are the highest-leverage model input in the repo. A wrong doc is worse than a missing one. |
|
|
||||||
|
|
||||||
### 5.4 Proposals
|
|
||||||
|
|
||||||
Ordered by value per line of work. Each is a Stage 0.5 item unless noted.
|
|
||||||
|
|
||||||
**S1. Require a statement terminator.** A statement ends at a newline, `;`, or
|
|
||||||
`}`. Make `T_NL` significant inside `block()` instead of discarding it.
|
|
||||||
*Why:* fixes R1, and it is the precondition for error recovery — without it a
|
|
||||||
parser cannot resynchronise, so every syntax error stays a cascade.
|
|
||||||
*Cost:* ~30 lines. *Ripple:* one-line bodies like `if x { a }` still work;
|
|
||||||
multi-statement one-liners in the runtime need a `sed`.
|
|
||||||
|
|
||||||
**S2. Make separators mandatory.** Commas required in every comma-list;
|
|
||||||
remove the optional path. *Fixes R2. Cost:* ~10 lines + tree-wide `sed`.
|
|
||||||
|
|
||||||
**S3. One spelling for boolean operators. ✅ LANDED.** `and`/`or` are the only
|
|
||||||
boolean operators. Ludic already spells bitwise operations as functions
|
|
||||||
(`band`/`bor`), so the symbols bought nothing, and dropping them removes the
|
|
||||||
`&` vs `&&` bug class by construction.
|
|
||||||
|
|
||||||
What shipped: `&&`/`||` still *lex* as single tokens, purely so the parser can
|
|
||||||
emit `'&&' is not a Ludic operator - write 'and'` instead of tripping over a
|
|
||||||
stray `&`; `and`/`or` became reserved words, so `let and = 5` is rejected at the
|
|
||||||
mistake; the AST op string is now `"and"`/`"or"`, which is **exactly the LLVM
|
|
||||||
opcode**, so the lowering ternary collapsed to passing `op` straight through;
|
|
||||||
and `--fmt` emits the new spelling for free, since it prints the op string.
|
|
||||||
Six regression tests in `bin/x test` (64 → 70), including one asserting no `.ludic`
|
|
||||||
source uses the symbols outside a comment. *Fixed R3.*
|
|
||||||
|
|
||||||
**S4. Reserve every keyword.** One table, shared by the lexer, parser,
|
|
||||||
`ludic-fmt` and `ludic-lsp` — those tools already share a vocabulary in
|
|
||||||
`ludic_syntax.h`, so there is one obvious home. Reject `let query = 5` at the
|
|
||||||
point of the mistake. *Fixes R5. Cost:* ~40 lines.
|
|
||||||
|
|
||||||
**S5. Delete or implement every silent no-op.** ← **highest value in the
|
|
||||||
section.** Two honest options per construct, no third:
|
|
||||||
|
|
||||||
- `reads` / `writes`: **implement them.** The compiler already knows every
|
|
||||||
property a system touches — it builds the query and walks the body. Checking
|
|
||||||
the declaration against actual access is a genuine static analysis the
|
|
||||||
language claims to have and doesn't. This converts dead syntax into a real
|
|
||||||
guarantee, which is exactly what an "AI-first" language should offer a model
|
|
||||||
reasoning about a system in isolation.
|
|
||||||
- `requires` / `ensures`: either lower to a checked assertion in debug builds
|
|
||||||
(`if !cond { print_err(...) os_exit(1) }` — cheap, and A6 stderr lands in
|
|
||||||
Stage 0 anyway), or remove them from the grammar until they mean something.
|
|
||||||
- `pure`, `needs`, `uses`, `effects`, `invariant`: remove until implemented.
|
|
||||||
|
|
||||||
*Fixes R6. Cost:* ~150 lines for `reads`/`writes` checking, ~60 for assertions,
|
|
||||||
~10 to delete the rest.
|
|
||||||
|
|
||||||
**S6. Cut the block grammars from seven to two.** Full unification is too
|
|
||||||
invasive to be worth it. The achievable version: every `{ }` is either a
|
|
||||||
**statement block** or a **field list** (`name: type = default`, comma-separated,
|
|
||||||
one shape), and `ui` props adopt the same separator rule as everything else.
|
|
||||||
Document all remaining shapes in one grammar table. *Partially fixes R4.
|
|
||||||
Cost:* ~120 lines.
|
|
||||||
|
|
||||||
**S7. One formatter with one contract.** Merge the philosophies rather than
|
|
||||||
keeping two half-tools: `ludic-fmt` gains `--fmt`'s normalisation decisions
|
|
||||||
(statement-per-line, single spelling, consistent commas) while keeping its
|
|
||||||
token-based comment preservation, and becomes **normative** — `ludic-fmt
|
|
||||||
--check` gates CI. `ludicc --fmt` reverts to being an honest debug dump and is
|
|
||||||
renamed `--dump-ast`. *Fixes R8.* After S1–S3, canonical form is the *only*
|
|
||||||
form, so the formatter stops being a style preference and becomes a check.
|
|
||||||
*Cost:* ~200 lines, mostly in `ludic_fmt.h`.
|
|
||||||
|
|
||||||
**S8. Machine-readable grammar and diagnostics.** Emit the grammar as one EBNF
|
|
||||||
file, and give every diagnostic a stable code plus a one-line suggested fix
|
|
||||||
(`ludicc --explain L0412`). Feeds the LSP, the docs and any model at once.
|
|
||||||
*Cost:* ~250 lines. *Defer to after the fixpoint* — valuable, not ordering-critical.
|
|
||||||
|
|
||||||
**S9. Documentation hygiene as a build step.** `bin/x test` already understands
|
|
||||||
` ```ludic ` fences. Extend it so **every fence in every `.md` must compile**,
|
|
||||||
and fix R10's stale claims. *Cost:* ~60 lines of shell. Do this early — it is
|
|
||||||
cheap and it stops the docs drifting further while the rest of the work lands.
|
|
||||||
|
|
||||||
### 5.5 What not to change
|
|
||||||
|
|
||||||
Recording these so they are not relitigated:
|
|
||||||
|
|
||||||
- **Braces, not indentation** (§5.2).
|
|
||||||
- **`#` comments** — unambiguous, one spelling already.
|
|
||||||
- **The ECS vocabulary** — `property` / `system` / `query` / `phase` are
|
|
||||||
unusually self-describing and greppable. This is the language's best existing
|
|
||||||
readability asset.
|
|
||||||
- **`fixed` / Q16.16** — determinism is a design constraint, not a style choice.
|
|
||||||
- **Do not add** operator overloading, implicit conversions beyond `int`→`fixed`,
|
|
||||||
macros, or anything else with action-at-a-distance. Every one of them trades
|
|
||||||
local readability for cleverness.
|
|
||||||
|
|
||||||
### 5.6 Sequencing
|
|
||||||
|
|
||||||
| When | What | Why there |
|
|
||||||
|---|---|---|
|
|
||||||
| **Now, before Stage 1** | S9 (doc hygiene) | Cheap; stops further drift immediately. |
|
|
||||||
| **Stage 0.5** | ~~S3~~ ✅ done · S1, S2, S4, S5, S6, S7 | Must precede the port (§5.1). |
|
|
||||||
| **After Stage 3** | S8 (EBNF + diagnostic codes) | Valuable, not ordering-critical; better written in Ludic against the self-hosted parser. |
|
|
||||||
|
|
||||||
**S3 was the one genuinely contentious call** — which boolean spelling — because
|
|
||||||
it is pure taste and touches every file. It was decided in favour of `and`/`or`
|
|
||||||
and has landed. Everything remaining in this section is a choice between "one
|
|
||||||
spelling" and "two", where the answer is not in doubt.
|
|
||||||
|
|
||||||
**`!` → `not` has since landed too**, on the same reasoning and by the same
|
|
||||||
mechanism: `!` still lexes (so `!=` is untouched) purely so the parser can say
|
|
||||||
`'!' is not a Ludic operator - write 'not'`. Ludic's three boolean operators are
|
|
||||||
now `and`, `or`, `not`, all reserved words, with no symbol spellings at all.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.7 Status — self-hosting achieved
|
|
||||||
|
|
||||||
Updated 2026-08-27. `bin/x test` = 93/93, `bin/x test-tools` = 28/28,
|
|
||||||
`bin/x selfhost-test` = 5/5 including the bootstrap fixpoint.
|
|
||||||
|
|
||||||
**Ludic is fully self-hosted.** The compiler is written in Ludic
|
|
||||||
(`selfhost/*.ludic`, ~2,400 lines), compiles every example to byte-identical
|
|
||||||
output and its own source to a fixpoint, and is built from a checked-in IR seed
|
|
||||||
with **no C compiler** — the former C compiler has been deleted.
|
|
||||||
|
|
||||||
From a clean checkout, build the compiler and the task-runner in one line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
|
||||||
```
|
|
||||||
|
|
||||||
Thereafter `bin/x build` rebuilds the entire toolchain into `bin/` (`ludicc`,
|
|
||||||
`ludic`, `x`, `ludic-fmt`, `ludic-lsp`), `bin/x bootstrap-cfree` reproduces the
|
|
||||||
compiler from the seed with no C compiler, and `bin/x help` lists every command.
|
|
||||||
Run `bin/x` from the repository root.
|
|
||||||
|
|
||||||
| Stage | What | State |
|
|
||||||
|---|---|---|
|
|
||||||
| **0** | language features (argv, struct, arrays/slices, break/continue, mem_realloc, stderr) | ✅ done |
|
|
||||||
| **0.5** | S3 (`and`/`or`/`not`), S9 (doc checking), short-circuit `and`/`or` | ✅ done |
|
|
||||||
| — | S1/S2/S4/S5/S6/S7 (statement terminators, mandatory commas, reserved-word audit, no-op removal, block unification, one formatter) | not done — polish of the *full* language, not needed for self-hosting |
|
|
||||||
| **1** | support libraries in Ludic (`str`, `buf`, `io`) | ✅ done |
|
|
||||||
| **2** | the compiler ported to Ludic (`lex`, `parse`, `emit_*`) | ✅ done |
|
|
||||||
| **3** | the fixpoint (`gen2.ll == gen3.ll`) | ✅ done |
|
|
||||||
| **4** | retire the C as a *live dependency* (IR seed, C-free rebuild) | ✅ done — `bin/x bootstrap-cfree` |
|
|
||||||
| **4+** | retire `ludicc.c` entirely (port the game backend) | ✅ **done** — `compiler/` deleted; the compiler is `selfhost/*.ludic` |
|
|
||||||
|
|
||||||
### What "self-hosting" means here, precisely
|
|
||||||
|
|
||||||
The self-host compiler (`selfhost/`) implements the **compiler-subset**: `struct`
|
|
||||||
(reference), `[]T` slices with `push`/`len`, functions, a plain `main` entry,
|
|
||||||
the full control flow, the operators (with short-circuit `and`/`or`), and the
|
|
||||||
low-level intrinsics. It deliberately does **not** implement the game half of
|
|
||||||
Ludic — ECS, queries, models, scenes, UI, save/load, `match`/`machine`,
|
|
||||||
fixed-point. It targets native (macOS/clang) and emits LLVM IR text that clang
|
|
||||||
assembles, exactly the posture the C `ludicc` has.
|
|
||||||
|
|
||||||
It is written entirely in that subset, which is why it compiles itself. The
|
|
||||||
three-generation proof (`bin/x bootstrap`):
|
|
||||||
|
|
||||||
```
|
|
||||||
stage0 build/ludicc (C) compiles selfhost.ludic -> gen1 (a Ludic-written compiler)
|
|
||||||
stage1 gen1 compiles selfhost.ludic -> gen2.ll -> gen2
|
|
||||||
stage2 gen2 compiles selfhost.ludic -> gen3.ll
|
|
||||||
assert gen2.ll == gen3.ll # the compiler reproduces itself, independent of its seed
|
|
||||||
```
|
|
||||||
|
|
||||||
`gen1`'s IR legitimately differs (a different compiler built it); `gen2 == gen3`
|
|
||||||
is the property that matters — the Ludic compiler has no dependency on how it was
|
|
||||||
built. It is also verified *correct*, not merely self-consistent: it compiles a
|
|
||||||
corpus (`selfhost/tests/`) of struct, slice, and control-flow programs to
|
|
||||||
binaries that produce the expected output.
|
|
||||||
|
|
||||||
### Stage 4 — the C is retired as a live dependency
|
|
||||||
|
|
||||||
The self-hosted compiler no longer needs the C `ludicc` to exist. Its own LLVM
|
|
||||||
IR is checked in as `selfhost/ludicc.seed.ll` — a proven fixed point — and
|
|
||||||
`bin/x bootstrap-cfree` assembles that with clang (an IR assembler, the
|
|
||||||
floor Rust and Swift stand on) and rebuilds the compiler, which reproduces its
|
|
||||||
own IR. **The C source is never invoked.** This is the seed path §8 recommended.
|
|
||||||
|
|
||||||
Crucially, the compiler **evolves** without the C compiler: `bin/x reseed`
|
|
||||||
uses the *current* seed to build a compiler with new source, then takes that
|
|
||||||
compiler's own output as the new seed. New features (this session: `match`,
|
|
||||||
bitwise ops, `peek32`/`poke32`) landed and reseeded entirely C-free. The C
|
|
||||||
compiler is now a historical seed, not a dependency.
|
|
||||||
|
|
||||||
### Stage 4+ — `ludicc.c` is deleted
|
|
||||||
|
|
||||||
The self-host compiler was extended to the **whole** language — properties,
|
|
||||||
models, systems, phases, `for … in query` (with `where`), spawn/despawn,
|
|
||||||
`self()`, `machine`/`become`, `match`, `save`/`load` snapshots, the retained
|
|
||||||
`ui` widget tree, multi-file `import`, fixed-point Q16.16, and every runtime
|
|
||||||
intrinsic. It auto-splices the Ludic runtime exactly as the C compiler did.
|
|
||||||
|
|
||||||
It now compiles **every example** — `snake`, `menu`, and the 6-file JRPG
|
|
||||||
`chronorift` — to output byte-identical to the original C compiler (checked
|
|
||||||
against golden renders in `selfhost/golden/`), and still compiles its own source
|
|
||||||
to a fixpoint. The C compiler (`compiler/`, ~2,700 lines) has been **deleted**.
|
|
||||||
`bin/x build` builds `bin/ludicc` from the IR seed with clang, and `bin/x app`
|
|
||||||
drives the native link (headless, or windowed via `cocoa.ll`).
|
|
||||||
|
|
||||||
What did not come across: the old C driver's **wasm target, cross-compilation,
|
|
||||||
and shared-library** paths. Those are driver features, not codegen — the
|
|
||||||
self-host compiler emits native-ABI IR — and re-implementing them on the
|
|
||||||
self-hosted toolchain (wasm needs i32 `size_t`; the others are clang flags in
|
|
||||||
the `bin/x app` build path) is the remaining follow-up.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. The plan
|
|
||||||
|
|
||||||
### Stage 0 — Extend the C compiler (~500 lines of C)
|
|
||||||
|
|
||||||
The C `ludicc` must be able to compile the Ludic `ludicc`. Land Tier A only, in
|
|
||||||
this order — cheapest-and-unblocking first:
|
|
||||||
|
|
||||||
1. **A4** `break`/`continue` (~40) — immediate readability win on everything after.
|
|
||||||
2. **A5** `mem_realloc` (~6).
|
|
||||||
3. **A1** `os_argc`/`os_arg` (~30) — unblocks the entire notion of a CLI tool.
|
|
||||||
4. **A6** `file_stderr` (~40).
|
|
||||||
5. **A2 + A3** `struct` + arrays (~400, landed together).
|
|
||||||
|
|
||||||
Each gets a test in `bin/x test` as it lands. The suite is at 64/64; Stage 0 should
|
|
||||||
leave it green and larger.
|
|
||||||
|
|
||||||
**Explicitly not in Stage 0:** function pointers, 64-bit ints, a `tool` entry
|
|
||||||
form. They are not on the path to the fixpoint.
|
|
||||||
|
|
||||||
### Stage 0.5 — Syntax freeze (~600 lines of C + a tree-wide `sed`)
|
|
||||||
|
|
||||||
**The grammar must be final before Stage 2 starts** (§5.1): after the fixpoint,
|
|
||||||
every syntax change costs a four-step reseed instead of an afternoon.
|
|
||||||
|
|
||||||
Land S1–S7 from §5.4: mandatory statement terminators, mandatory separators,
|
|
||||||
one boolean spelling, reserved keywords, no silent no-ops, two block shapes
|
|
||||||
instead of seven, one normative formatter. S9 (doc hygiene) can land earlier —
|
|
||||||
it is cheap and independent.
|
|
||||||
|
|
||||||
Exit criterion: `ludic-fmt --check` passes on the whole tree and there is
|
|
||||||
exactly one legal spelling of every construct. That is also what makes the
|
|
||||||
Stage 2b oracle tight.
|
|
||||||
|
|
||||||
### Stage 1 — Support libraries in Ludic (~800 lines of Ludic, zero language work)
|
|
||||||
|
|
||||||
Nothing here needs Stage 0 except `struct`/arrays for pleasantness. This is the
|
|
||||||
part that is pure writing, and it can start immediately and in parallel.
|
|
||||||
|
|
||||||
| File | Contents |
|
|
||||||
|---|---|
|
|
||||||
| `runtime/native/strings.ludic` | `str_eq`, `str_len`, `str_dup`, `str_cat`, `substr`, `str_chr`, `str_hash`, `itoa`, `atoi`, `hex` |
|
|
||||||
| `runtime/native/buf.ludic` | growable byte buffer — `buf_new`, `buf_putc`, `buf_puts`, `buf_putint`, `buf_len`, `buf_ptr`. This is `SB` from `ludicc.c`, and the IR emitter is nothing but calls to it. |
|
|
||||||
| `runtime/native/io.ludic` | `read_whole_file` (the open/seek/tell/read/close dance, verified working), `write_whole_file`, stderr diagnostics |
|
|
||||||
| `runtime/native/map.ludic` | open-addressing `str -> int` hash table: keyword lookup, string interning, symbol tables |
|
|
||||||
| `runtime/native/arena.ludic` | bump allocator — see §8 |
|
|
||||||
|
|
||||||
### Stage 2 — Port the compiler, each piece against a differential oracle
|
|
||||||
|
|
||||||
Port in dependency order. The critical discipline: **never port a stage without
|
|
||||||
an automated way to prove it agrees with the C one.** Ludic is unusually well
|
|
||||||
set up for this, because it already ships two canonical serializers of compiler
|
|
||||||
internals.
|
|
||||||
|
|
||||||
| Sub-stage | Port | Differential oracle |
|
|
||||||
|---|---|---|
|
|
||||||
| 2a | `lex.ludic` | Dump the token stream from both compilers; `diff` over every `.ludic` in the tree. |
|
|
||||||
| 2b | `parse.ludic` (AST) | **`--fmt` is a free oracle.** The formatter is already a canonical AST printer, and `bin/x test` already asserts formatting never changes a program. If both compilers' `--fmt` output is byte-identical on every file, the parsers agree. |
|
|
||||||
| 2c | `check.ludic` | Diagnostic text must match on a corpus of deliberately-broken programs. `bin/x test` already checks diagnostics — extend that corpus. |
|
|
||||||
| 2d | `emit.ludic` (IR) | **`--emit llvm` must be byte-identical** for every example. This is the strongest oracle available: pass/fail on exact text, no judgement. |
|
|
||||||
| 2e | `drive.ludic` | Assemble and link via `clang`; compare final binaries. |
|
|
||||||
|
|
||||||
Sub-stage 2b deserves emphasis. Most self-hosting projects have no cheap way to
|
|
||||||
prove two parsers agree. Ludic has one already built and already tested, which
|
|
||||||
removes the single largest source of silent divergence.
|
|
||||||
|
|
||||||
### Stage 3 — The fixpoint
|
|
||||||
|
|
||||||
```
|
|
||||||
stage1 = C-ludicc compiles ludicc.ludic -> binary A
|
|
||||||
stage2 = A compiles ludicc.ludic -> binary B
|
|
||||||
stage3 = B compiles ludicc.ludic -> binary C
|
|
||||||
|
|
||||||
assert B == C byte-for-byte <- THE bootstrap test
|
|
||||||
```
|
|
||||||
|
|
||||||
`A != B` is expected and correct: `A` was built by a different compiler, so its
|
|
||||||
codegen differs. `B == C` is the real property — a compiler that reproduces
|
|
||||||
itself has no dependency on how it was built. Also assert that `A`, `B` and `C`
|
|
||||||
all emit identical IR for every example.
|
|
||||||
|
|
||||||
If `B != C`, the cause is almost always nondeterminism in the compiler itself:
|
|
||||||
hash-table iteration order, an address baked into output, uninitialised memory.
|
|
||||||
Those are worth hunting rather than working around.
|
|
||||||
|
|
||||||
### Stage 4 — Retire the C
|
|
||||||
|
|
||||||
Once the fixpoint holds, the C compiler becomes a seed. Options:
|
|
||||||
|
|
||||||
| Option | Trade-off |
|
|
||||||
|---|---|
|
|
||||||
| **Commit the generated `ludicc.ll`** ✅ recommended | Auditable text, diffable in review, builds with `clang` alone — already a dependency. Large but honest. |
|
|
||||||
| Commit prebuilt binaries per platform | Smallest process, worst auditability; a binary blob nobody can read. What Rust does. |
|
|
||||||
| Keep `ludicc.c` forever as the seed | Zero risk, but §1's bar is never met — the C never leaves. What Go did for years. |
|
|
||||||
|
|
||||||
Recommend the IR seed: it is the only option that both removes the C and leaves
|
|
||||||
a reviewer something to read.
|
|
||||||
|
|
||||||
`tools/ludic-tools/` (3,260 lines of C: `ludic-fmt`, `ludic-lsp`) is a separate
|
|
||||||
port and should follow, not lead — once the Ludic compiler exists, both tools
|
|
||||||
should be thin front-ends over its lexer and parser instead of maintaining a
|
|
||||||
second copy of the vocabulary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Toolchain independence — and where to stop
|
|
||||||
|
|
||||||
`driver.c` shells out to `clang` (overridable via `$LUDIC_CC`) to assemble IR
|
|
||||||
into an object and to link, plus `wasm-ld` for wasm. Three levels of removing
|
|
||||||
that, and only one is worth doing:
|
|
||||||
|
|
||||||
**Level 1 — self-hosted compiler, hosted toolchain. ← the goal.**
|
|
||||||
`ludicc` is Ludic; `clang` remains the IR assembler and linker driver. This is
|
|
||||||
exactly where Rust and Swift stand. Achieved at the end of Stage 4.
|
|
||||||
|
|
||||||
**Level 2 — own object writer.** Emit Mach-O / ELF / COFF directly, replacing
|
|
||||||
IR-text + `clang -c`. Requires instruction selection, register allocation and
|
|
||||||
relocations: realistically 5,000–15,000 lines of Ludic, and it *loses the LLVM
|
|
||||||
optimizer* — the generated code gets slower, which for a game language is a
|
|
||||||
real regression, not a neutral trade. **Not recommended.**
|
|
||||||
|
|
||||||
**Level 3 — own linker.** Platform-specific, deep, and buys nothing a user can
|
|
||||||
perceive. **No.**
|
|
||||||
|
|
||||||
**7.4 — The hand-written IR.** `cocoa.ll` (327 lines) and `wasm.ll` are LLVM IR,
|
|
||||||
not Ludic. Two defensible positions: keep them as *platform glue written in the
|
|
||||||
platform's own assembly language* (precisely how Rust uses `asm!` shims and how
|
|
||||||
every libc has hand-written syscall stubs), or move them into `.ludic` — which
|
|
||||||
needs **B1 function pointers**, because the `NSView` subclass installs a
|
|
||||||
function as an Objective-C IMP. Keeping them is the honest default; the README's
|
|
||||||
existing framing ("the same floor Rust and Swift stand on") already covers it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Risks and gotchas
|
|
||||||
|
|
||||||
- **Do not build the AST out of ECS entities.** `LUDIC_MAX_ENT` is 1024
|
|
||||||
(`native.c:18`) and property storage is fixed arrays. It compiles, it looks
|
|
||||||
elegant, and it caps the compiler at 1024 nodes. Use `struct` (A2).
|
|
||||||
- **Do not inherit the C compiler's fixed caps.** `ludicc.c:22` has
|
|
||||||
`g_srcpath[128]`; `native.c:161` has `Val a[8]`. The Ludic port should grow
|
|
||||||
its tables (A5) rather than reproduce the limits.
|
|
||||||
- **Leak on purpose.** A compiler runs once and exits. A bump arena
|
|
||||||
(`arena.ludic`) that never frees is faster and simpler than tracked
|
|
||||||
ownership, and it sidesteps having no GC. Free at process exit — i.e. never.
|
|
||||||
- **Determinism is a feature now.** Anything order-dependent — hash iteration,
|
|
||||||
pointer values in output, uninitialised reads — breaks `B == C` in Stage 3.
|
|
||||||
Iterate tables in insertion order, not bucket order.
|
|
||||||
- **Error handling has no exceptions.** Mirror the C `die()`: write the
|
|
||||||
diagnostic to stderr (A6), then `os_exit(1)`.
|
|
||||||
- **The `str + str` mismatch (B2)** is a live example of the front-end accepting
|
|
||||||
what the backend cannot lower. Worth auditing for siblings before trusting
|
|
||||||
the typechecker as a Stage 2c oracle.
|
|
||||||
- **Size-taking intrinsics must use `ll_size_t()` / `ll_widen()`.** `size_t` is
|
|
||||||
`i32` on wasm32. Any new intrinsic with a size argument (A5) that hardcodes
|
|
||||||
`i64` will break the wasm target at link time.
|
|
||||||
- **Recursion depth is fine** — 5,000 frames verified, well past what a
|
|
||||||
recursive-descent parser needs on real source.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Effort
|
|
||||||
|
|
||||||
| Stage | Work | State |
|
|
||||||
|---|---|---|
|
|
||||||
| 0 | Tier A language features (argv, struct, slices, break/continue, mem_realloc, stderr) | ✅ done |
|
|
||||||
| 0.5 | `and`/`or`/`not` + short-circuit; doc checking (S9) | ✅ done (S1/S2/S4/S5/S6/S7 deferred — full-language polish) |
|
|
||||||
| 1 | `str`, `buf`, `io` support libraries in Ludic | ✅ done (`selfhost/`) |
|
|
||||||
| 2 | lexer + parser + AST + IR emitter, in Ludic | ✅ done (`selfhost/`, ~1,300 lines) |
|
|
||||||
| 3 | the fixpoint (`gen2.ll == gen3.ll`) + harness | ✅ done (`bin/x bootstrap`) |
|
|
||||||
| 4 | port the game backend, retire `ludicc.c` | ⛔ out of scope — mechanical continuation |
|
|
||||||
|
|
||||||
The self-host compiler is **~1,300 lines of Ludic** covering the compiler-subset.
|
|
||||||
A `main`-tool entry point and short-circuit `and`/`or` were the two language
|
|
||||||
additions that made it self-compilable; the rest of Stage 0 was already in place.
|
|
||||||
|
|
||||||
Roughly **6,000 lines of Ludic and 1,100 lines of C** to reach Level 1 — larger
|
|
||||||
than the 2,508-line C compiler it replaces, which is normal: the C version leans
|
|
||||||
on libc for everything in Stage 1.
|
|
||||||
|
|
||||||
**Two independent critical paths, and they can run in parallel.** Stage 0 + Stage 0.5
|
|
||||||
are C work on the existing compiler; Stage 1 is Ludic work that needs almost none of
|
|
||||||
it. The only hard barrier is that Stage 2 starts after *both*.
|
|
||||||
|
|
||||||
**The critical path is short.** A1 (argv, ~30 lines of C) plus A2/A3
|
|
||||||
(`struct` + arrays, ~400) plus A4 (`break`, ~40) is nearly all the *design* risk
|
|
||||||
in the project. Everything after it is typing against oracles that already
|
|
||||||
exist.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix — probe programs
|
|
||||||
|
|
||||||
Each was compiled with `bin/x app probe.ludic --headless` and run against the
|
|
||||||
current tree (`bin/x test` = 64/64).
|
|
||||||
|
|
||||||
**Recursion** ✅ → `55`
|
|
||||||
```ludic
|
|
||||||
program P {
|
|
||||||
function fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) }
|
|
||||||
handler B phase Start { print_int(fib(10)); quit() }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**String comparison, hand-written** ✅ → `1`
|
|
||||||
```ludic
|
|
||||||
function streq(a: pointer, b: pointer) -> bool {
|
|
||||||
let i = 0
|
|
||||||
while true {
|
|
||||||
let ca = peek8(a,i)
|
|
||||||
let cb = peek8(b,i)
|
|
||||||
if ca != cb { return false }
|
|
||||||
if ca == 0 { return true }
|
|
||||||
i = i + 1
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Integer → string, hand-written** ✅ → `48291`
|
|
||||||
```ludic
|
|
||||||
function itoa(v: int, buf: pointer) -> int {
|
|
||||||
let n = 0
|
|
||||||
let x = v
|
|
||||||
if x == 0 { poke8(buf,0,48); return 1 }
|
|
||||||
let tmp = mem_alloc(16)
|
|
||||||
while x > 0 { poke8(tmp, n, 48 + x % 10); x = x / 10; n = n + 1 }
|
|
||||||
let i = 0
|
|
||||||
while i < n { poke8(buf, i, peek8(tmp, n-1-i)); i = i + 1 }
|
|
||||||
mem_free(tmp)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Read a whole file** ✅ → the compiler's front door
|
|
||||||
```ludic
|
|
||||||
let f = file_open("/tmp/x.txt", "rb")
|
|
||||||
file_seek(f, 0, 2)
|
|
||||||
let n = file_tell(f)
|
|
||||||
file_seek(f, 0, 0)
|
|
||||||
let b = mem_alloc(n+1)
|
|
||||||
file_read(f, b, n)
|
|
||||||
poke8(b, n, 0)
|
|
||||||
file_close(f)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Syntax audit — every one of these compiles today
|
|
||||||
|
|
||||||
Each is a spelling the language accepts; the point is that the *alternative*
|
|
||||||
spelling is equally legal (§5.3).
|
|
||||||
|
|
||||||
**R1 — statements now require a separator (Rule B, syntax-redesign Phase 2)** → parse error
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — intentionally rejected under Rule B: needs a newline or ';'
|
|
||||||
program P { handler B phase Start { let x = 1 x = x + 1 print_int(x) quit() } }
|
|
||||||
```
|
|
||||||
Statements no longer sit adjacent with only spaces between them; the compiler
|
|
||||||
reports `expected newline or ';' between statements`. Put each on its own line,
|
|
||||||
or separate them with `;` (both lex to the same separator token):
|
|
||||||
```ludic
|
|
||||||
program P { handler B phase Start { let x = 1; x = x + 1; print_int(x); quit() } }
|
|
||||||
```
|
|
||||||
|
|
||||||
**R2 — commas omitted throughout** → `7`
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — composite: declaration plus statements
|
|
||||||
property Pos { x: int = 0 y: int = 0 }
|
|
||||||
spawn Hero { Pos { x: 7 y: 2 } }
|
|
||||||
```
|
|
||||||
|
|
||||||
**R3 — RESOLVED.** Every symbol form is now rejected where it is written:
|
|
||||||
```
|
|
||||||
ludicc: error: line 1: '&&' is not a Ludic operator - write 'and' (got '&&')
|
|
||||||
ludicc: error: line 1: '||' is not a Ludic operator - write 'or' (got '||')
|
|
||||||
ludicc: error: line 1: '!' is not a Ludic operator - write 'not' (got '!')
|
|
||||||
ludicc: error: line 1: 'and' is a reserved operator and cannot be used as a name
|
|
||||||
```
|
|
||||||
`--fmt` prints `if ((true and false) or (1 < 2))` and `(not true)`, while unary
|
|
||||||
minus keeps its tight spelling `(-x)`. `!=` is untouched.
|
|
||||||
|
|
||||||
**R5 — reserved-looking words used as locals** → `11`
|
|
||||||
```ludic
|
|
||||||
let query = 5
|
|
||||||
let phase = 6
|
|
||||||
print_int(query + phase)
|
|
||||||
```
|
|
||||||
|
|
||||||
**R6 — contracts accepted and discarded.** Both are violated; it compiles and
|
|
||||||
prints `4`:
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — composite: declaration plus statements
|
|
||||||
function half(n: int) -> int requires n > 100000 ensures false { return n / 2 }
|
|
||||||
print_int(half(8))
|
|
||||||
```
|
|
||||||
And a system may declare read-only access, then write — also compiles:
|
|
||||||
```ludic
|
|
||||||
handler Violate phase Update reads [Pos] query (p) [Pos] { p.x = 99 }
|
|
||||||
```
|
|
||||||
|
|
||||||
**R8 — the two formatters disagree about what "format" means.** Given
|
|
||||||
`property Pos { x: int = 0 y: int = 0 }` and a multi-statement one-liner,
|
|
||||||
`ludicc --fmt` rewrites both (one statement per line, `and`→`&&`, full
|
|
||||||
parenthesisation) while `ludic-fmt` returns the input **unchanged**.
|
|
||||||
|
|
||||||
**Verified-missing** — each a compile error today:
|
|
||||||
```ludic
|
|
||||||
# doc-check: expect-error — every line here is a compile error by design
|
|
||||||
while i < 10 { i = i + 1; if i == 3 { break } } # unknown identifier 'break'
|
|
||||||
struct Node { k: int, a: pointer } # expected declaration (got 'struct')
|
|
||||||
var t: [int; 8] # expected identifier (got '[')
|
|
||||||
var h: fn = a # unknown type 'fn' for var h
|
|
||||||
let p = &cb # fails to compile
|
|
||||||
print_int(os_argc()) # unknown function 'os_argc'
|
|
||||||
print_err("x") # unknown function 'print_err'
|
|
||||||
let p = mem_realloc(ptr_null(), 10) # unknown function 'mem_realloc'
|
|
||||||
print_str("ab" + "cd") # passes front-end, invalid IR
|
|
||||||
let a = 100000; print_int(a*100000) # 1410065408 — i32 wrap
|
|
||||||
```
|
|
||||||
1262
CHANGELOG.md
1262
CHANGELOG.md
File diff suppressed because it is too large
Load diff
119
COMPILING.md
119
COMPILING.md
|
|
@ -1,37 +1,46 @@
|
||||||
# Compiling Ludic
|
# Compiling Ludic
|
||||||
|
|
||||||
> **Note (2026-08-27):** `ludicc` is now **written in Ludic** (`selfhost/*.ludic`)
|
> **Note:** `ludicc` is **written in Ludic** (`selfhost/*.ludic`) and built from a
|
||||||
> and built from a checked-in IR seed — the C compiler this document describes has
|
> checked-in IR seed — the C compiler this document once described has been
|
||||||
> been deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is
|
> deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is
|
||||||
> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so
|
> unchanged. `ludicc` drives clang itself (via an `os_system` intrinsic), so
|
||||||
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling
|
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly. `--fmt` is
|
||||||
> command `ludic app.ludic` compiles to a temporary binary and runs it in one
|
> reimplemented as a lex+parse gate (the doc-check hook). The `--target`/
|
||||||
> step. The whole toolchain is built by `bin/x build`; `bin/x app` remains as a
|
> cross-compile and `--shared` paths are still features of the old C driver not
|
||||||
> convenience wrapper over the compiler. `--fmt` is reimplemented as a lex+parse
|
> yet re-implemented on the self-hosted toolchain. See the
|
||||||
> gate (the doc-check hook). The `--target`/cross-compile and `--shared` paths are
|
> [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap) §5.7 on the wiki.
|
||||||
> still features of the old C driver not yet re-implemented on the self-hosted
|
|
||||||
> toolchain. See BOOTSTRAP.md §5.7.
|
|
||||||
>
|
>
|
||||||
> From a clean checkout, build the compiler and the task-runner in one line, then
|
> Most people never invoke `ludicc` directly: the `ludic` CLI drives it.
|
||||||
> let `bin/x` do the rest (run it from the repository root):
|
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/x
|
> curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh # the toolchain, into ~/.ludic
|
||||||
> clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
> ludic new mygame && cd mygame
|
||||||
> bin/x build # rebuild the whole toolchain into bin/
|
> ludic run # compile + run
|
||||||
> # (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
> ludic build --headless # compile, deterministic render
|
||||||
> bin/ludicc examples/games/snake.ludic -o bin/snake # compile
|
|
||||||
> bin/ludic examples/games/snake.ludic # compile + run
|
|
||||||
> bin/x help # list every command
|
|
||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> The binaries are multi-call (one native binary under two names): invoked as
|
> From a clean checkout, the compiler and the CLI come up in two lines and the
|
||||||
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
|
> CLI does the rest (run it from the repository root):
|
||||||
> systems is a game and links windowed by default; `--headless` and `--windowed`
|
>
|
||||||
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
|
> ```bash
|
||||||
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in
|
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic
|
||||||
> `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
|
> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
> assembler/linker (default `clang`).
|
> bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
|
> bin/ludic-dev build # the whole toolchain into bin/
|
||||||
|
> # (ludicc, ludic, ludic-fmt, ludic-lsp)
|
||||||
|
> bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly
|
||||||
|
> bin/ludic build examples/games/snake.ludic # or through the CLI
|
||||||
|
> bin/ludic help # every command
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> A `.ludic` file with handlers is a game and links windowed by default;
|
||||||
|
> `--headless` and `--windowed` force the mode. The engine runtime
|
||||||
|
> (`runtime/native/cocoa.ll`, the spliced `runtime/native/*.ludic`) and the
|
||||||
|
> bundled `ludic.*` packages are found under the **install root**: `$LUDIC_HOME`
|
||||||
|
> if set, otherwise derived from the binary's own location — the parent of its
|
||||||
|
> `bin/` directory, which is both `~/.ludic` for an install and the repository
|
||||||
|
> root for a checkout. `$LUDIC_CC` overrides the assembler/linker (default
|
||||||
|
> `clang`).
|
||||||
|
|
||||||
|
|
||||||
`ludicc` is a compiler, not a translator. It lexes, parses, checks and lowers
|
`ludicc` is a compiler, not a translator. It lexes, parses, checks and lowers
|
||||||
|
|
@ -42,10 +51,10 @@ and find your program rewritten in another language.
|
||||||
|
|
||||||
```
|
```
|
||||||
app.ludic
|
app.ludic
|
||||||
│ ludicc — lex, parse, check, lower (compiler/ludicc.c,
|
│ ludicc — lex, parse, lower (selfhost/frontend/*.ludic,
|
||||||
▼ compiler/native.c)
|
▼ selfhost/backend/*.ludic)
|
||||||
app.ll LLVM IR: your systems, your properties, your runtime
|
app.ll LLVM IR: your handlers, your properties, your runtime
|
||||||
│ IR assembler (compiler/driver.c)
|
│ IR assembler (selfhost/main.ludic drives $LUDIC_CC)
|
||||||
▼
|
▼
|
||||||
app.o Mach-O / ELF / COFF object code
|
app.o Mach-O / ELF / COFF object code
|
||||||
│ system linker
|
│ system linker
|
||||||
|
|
@ -64,6 +73,9 @@ point at a different LLVM toolchain if you have one.
|
||||||
| a windowed native executable | `ludicc game.ludic -o build/game` |
|
| a windowed native executable | `ludicc game.ludic -o build/game` |
|
||||||
| a headless executable | `ludicc game.ludic --headless -o build/game` |
|
| a headless executable | `ludicc game.ludic --headless -o build/game` |
|
||||||
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
|
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
|
||||||
|
| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` |
|
||||||
|
| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` |
|
||||||
|
| the same, with an unsaved buffer on stdin standing for one of its files | `ludicc src.ludic --check --diagnostics=json --stdin-file lib/a.ludic < buf` |
|
||||||
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
|
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
|
||||||
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
|
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
|
||||||
| an object file † | `ludicc src.ludic -c -o src.o` |
|
| an object file † | `ludicc src.ludic -c -o src.o` |
|
||||||
|
|
@ -73,32 +85,33 @@ the old C driver and are **not yet re-implemented** on the self-hosted toolchain
|
||||||
(see the note at the top). The rows above the line work today via the
|
(see the note at the top). The rows above the line work today via the
|
||||||
self-hosted `ludicc`.
|
self-hosted `ludicc`.
|
||||||
|
|
||||||
`bin/x app` wraps the common cases:
|
`bin/ludic build` wraps the common cases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x app examples/games/snake.ludic # -> build/snake (native)
|
bin/ludic build examples/games/snake.ludic # -> build/snake (native)
|
||||||
bin/x app examples/library/combat.ludic --lib # -> build/libcombat.* (library)
|
bin/ludic build examples/library/combat.ludic --lib # -> build/libcombat.* (library)
|
||||||
bin/x app examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
bin/ludic build examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
||||||
bin/x app examples/games/snake.ludic --web # -> build/web/ (browser)
|
bin/ludic build examples/games/snake.ludic --web # -> build/web/ (browser)
|
||||||
```
|
```
|
||||||
|
|
||||||
The `--lib` and `--web` targets were part of the old C driver and are **not yet
|
The `--lib` and `--web` targets were part of the old C driver and are **not yet
|
||||||
re-implemented** on the self-hosted toolchain — `bin/x app` supports the native
|
re-implemented** on the self-hosted toolchain — `bin/ludic build` supports the native
|
||||||
windowed and `--headless` builds today.
|
windowed and `--headless` builds today.
|
||||||
|
|
||||||
## Programs and libraries
|
## Programs and libraries
|
||||||
|
|
||||||
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
|
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
|
||||||
> workflow below describe the old C driver's behavior; the self-hosted `ludicc`
|
> workflow below describe the old C driver's behavior; the self-hosted `ludicc`
|
||||||
> builds executables only for now. The `module`/`@export fn` semantics are
|
> builds executables only for now. The `@export function` semantics are
|
||||||
> unchanged — only the packaging step is pending.
|
> unchanged — only the packaging step is pending.
|
||||||
|
|
||||||
A source file opens with `game Name { … }` or `module Name { … }`.
|
A source file opens with `program Name { … }`.
|
||||||
|
|
||||||
* A **game** gets an entry point and the phase-ordered frame loop
|
* A program with **handlers** is a game: it gets the phase-ordered frame loop
|
||||||
(`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick).
|
(`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick).
|
||||||
* A **module** gets neither. It is a library, and only its `@export fn`s become
|
* A program with only an **`entry`** block is a tool: it runs `entry` and exits.
|
||||||
public symbols; everything else stays private to the library.
|
* Either kind can be a library: only its `@export function`s become public
|
||||||
|
symbols; everything else stays private.
|
||||||
|
|
||||||
```ludic
|
```ludic
|
||||||
# doc-check: skip — illustrative: elided body
|
# doc-check: skip — illustrative: elided body
|
||||||
|
|
@ -196,8 +209,11 @@ intrinsics compile to nothing there, so a headless binary never references a
|
||||||
symbol the window would have provided.
|
symbol the window would have provided.
|
||||||
|
|
||||||
Other platforms build headless today. A Win32 or X11 port is another `.ll` file
|
Other platforms build headless today. A Win32 or X11 port is another `.ll` file
|
||||||
with the same five entry points — `win_open`, `win_poll`, `win_present`,
|
with the same entry points — the window (`win_open`, `win_poll`, `win_present`,
|
||||||
`win_running`, `win_close` — and no compiler change.
|
`win_running`, `win_close`), keys (`win_held`, `win_held_bit`), the mouse and
|
||||||
|
cursor (`win_mouse`, `win_cursor_mode`, `win_cursor_confine`,
|
||||||
|
`win_cursor_maintain`), gamepad (`win_pad`) and touch (`win_touch`) — and no
|
||||||
|
compiler change.
|
||||||
|
|
||||||
## The web
|
## The web
|
||||||
|
|
||||||
|
|
@ -219,7 +235,7 @@ only the triple changes.
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x app examples/games/chronorift.ludic --web
|
bin/ludic build examples/games/chronorift.ludic --web
|
||||||
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -307,7 +323,7 @@ node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss
|
||||||
```
|
```
|
||||||
|
|
||||||
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
|
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
|
||||||
and the wasm one must render byte-identical frames from the same input. `bin/x test`
|
and the wasm one must render byte-identical frames from the same input. `bin/ludic-dev test`
|
||||||
asserts exactly that, which is a much stronger check on the backend than
|
asserts exactly that, which is a much stronger check on the backend than
|
||||||
"it started".
|
"it started".
|
||||||
|
|
||||||
|
|
@ -321,13 +337,13 @@ entity allocator, save/load snapshots, the frame loop, the window, and the whole
|
||||||
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
|
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
|
||||||
the retained UI.
|
the retained UI.
|
||||||
|
|
||||||
None of it goes through C. `bin/x test` asserts that directly: no C source
|
None of it goes through C. `bin/ludic-dev test` asserts that directly: no C source
|
||||||
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
|
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
|
||||||
build, run and render from IR alone.
|
build, run and render from IR alone.
|
||||||
|
|
||||||
## Every flag
|
## Every flag
|
||||||
|
|
||||||
The self-hosted `ludicc`/`ludic` (built with `bin/x build-cli`) accept:
|
The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
|
||||||
|
|
||||||
```
|
```
|
||||||
<file.ludic> the program to compile (first non-flag argument)
|
<file.ludic> the program to compile (first non-flag argument)
|
||||||
|
|
@ -337,15 +353,18 @@ The self-hosted `ludicc`/`ludic` (built with `bin/x build-cli`) accept:
|
||||||
--windowed force a windowed (Cocoa) build
|
--windowed force a windowed (Cocoa) build
|
||||||
--headless force a headless build (stdin input, out.ppm output)
|
--headless force a headless build (stdin input, out.ppm output)
|
||||||
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
||||||
|
--check every check a build makes (types, modules, uses, layers, ports, binds); write nothing
|
||||||
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
||||||
(the check-docs gate; canonical formatting not yet restored)
|
(the check-docs gate; canonical formatting not yet restored)
|
||||||
--save-temps keep the intermediate .ll
|
--save-temps keep the intermediate .ll
|
||||||
--run compile then run (implicit when invoked as `ludic`)
|
--run compile then run (what `ludic run` uses)
|
||||||
(unknown -flags are ignored with a warning, never taken as the input file)
|
(unknown -flags are ignored with a warning, never taken as the input file)
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
LUDIC_CC the LLVM that assembles IR and drives the linker (clang)
|
LUDIC_CC the LLVM that assembles IR and drives the linker (clang)
|
||||||
LUDIC_HOME where runtime/native/ lives (default: the binary's dir)
|
LUDIC_HOME the install root — runtime/, packages/, VERSION
|
||||||
|
(default: the parent of the binary's bin/ directory)
|
||||||
|
LUDIC_MODULES the project's fetched packages (default: ./ludic_modules)
|
||||||
```
|
```
|
||||||
|
|
||||||
Mode is automatic when neither `--windowed` nor `--headless` is given: a program
|
Mode is automatic when neither `--windowed` nor `--headless` is given: a program
|
||||||
|
|
|
||||||
175
CONTRIBUTING.md
175
CONTRIBUTING.md
|
|
@ -18,18 +18,25 @@ runtime, and the tooling are all written in Ludic and built by Ludic.
|
||||||
From a clean checkout, one line lifts the toolchain off the seed:
|
From a clean checkout, one line lifts the toolchain off the seed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
|
bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
That gives you `bin/x`, the Ludic task runner that replaces every build/test
|
That gives you `bin/ludic-dev`, the contributor tool: it replaces every
|
||||||
shell script in the repo. From then on it builds everything — including itself:
|
build/test shell script in the repo and builds everything, including itself and
|
||||||
|
`bin/ludic`. It is deliberately a separate binary from the `ludic` users install
|
||||||
|
— that one carries none of these tasks and is never asked to.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x build # rebuild the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
bin/ludic-dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)
|
||||||
bin/x help # list every command
|
bin/ludic-dev help # every contributor task
|
||||||
|
bin/ludic help # what a user of the language sees
|
||||||
```
|
```
|
||||||
|
|
||||||
Always run `x` from the repository root, so `assets/` and `selfhost/` resolve.
|
Always run `ludic-dev` from the repository root, so `assets/` and `selfhost/`
|
||||||
|
resolve. (A checkout is also an install root: `bin/` beside `runtime/` and
|
||||||
|
`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`, which is
|
||||||
|
why `bin/ludic` behaves there exactly as an installed one does.)
|
||||||
|
|
||||||
## The development loop
|
## The development loop
|
||||||
|
|
||||||
|
|
@ -37,18 +44,18 @@ When you change the compiler or runtime, prove the self-hosting fixpoint still
|
||||||
holds before you push:
|
holds before you push:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
|
bin/ludic-dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
|
||||||
bin/x bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
|
bin/ludic-dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
|
||||||
bin/x test # the full regression suite
|
bin/ludic-dev test # the full regression suite
|
||||||
```
|
```
|
||||||
|
|
||||||
Other useful targets:
|
Other useful targets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x app <file.ludic> [--headless] # compile a program to a native app in build/
|
bin/ludic build <file.ludic> [--headless] # compile a program to a native app in build/
|
||||||
bin/x selfhost-test # correctness + bootstrap fixpoints
|
bin/ludic-dev selfhost-test # correctness + bootstrap fixpoints
|
||||||
bin/x test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
|
bin/ludic-dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
|
||||||
bin/x clean # remove build/, out.ppm and stray artifacts
|
bin/ludic clean # remove build/, out.ppm and stray artifacts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding to the standard library
|
## Adding to the standard library
|
||||||
|
|
@ -61,7 +68,7 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
|
||||||
and register its id in `tools/docgen/inventory.json`. Each documented
|
and register its id in `tools/docgen/inventory.json`. Each documented
|
||||||
namespace gets exactly **one** directory (the docs check enforces this).
|
namespace gets exactly **one** directory (the docs check enforces this).
|
||||||
3. Add or extend an example under `examples/` and a case in the test suite.
|
3. Add or extend an example under `examples/` and a case in the test suite.
|
||||||
4. Run `python3 tools/docgen/gen.py && python3 tools/docgen/check.py` — the
|
4. Run `bin/ludic-dev docs-gen --out build/pages && bin/ludic-dev docs-check build/pages` — the
|
||||||
check fails if any inventory symbol lacks a page or is still seed text.
|
check fails if any inventory symbol lacks a page or is still seed text.
|
||||||
5. Add a **changeset** for the user-facing change: a small file under
|
5. Add a **changeset** for the user-facing change: a small file under
|
||||||
[`changes/`](changes/README.md) with a `bump:` level and a one-line summary.
|
[`changes/`](changes/README.md) with a `bump:` level and a one-line summary.
|
||||||
|
|
@ -70,20 +77,105 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
|
||||||
## Versioning & releases
|
## Versioning & releases
|
||||||
|
|
||||||
The toolchain is versioned with [SemVer](https://semver.org); `VERSION` is the
|
The toolchain is versioned with [SemVer](https://semver.org); `VERSION` is the
|
||||||
single source of truth and `ludicc --version` (or `x version`) reports it.
|
single source of truth and `ludicc --version` (or `ludic version`) reports it.
|
||||||
|
|
||||||
Releases are changeset-driven. Every user-facing change ships with a changeset
|
Releases are changeset-driven. Every user-facing change ships with a changeset
|
||||||
(step 5 above). To cut a release:
|
(step 5 above). Read the next release before cutting it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
x release [major|minor|patch] # omit the level to derive it from the changesets
|
ludic-dev release --dry-run # render the CHANGELOG section, write nothing
|
||||||
```
|
```
|
||||||
|
|
||||||
That aggregates the pending changesets into a new `CHANGELOG.md` section, bumps
|
Then cut it:
|
||||||
`VERSION`, commits `chore(release): vX.Y.Z`, and tags it. Add `--publish` (with
|
|
||||||
`FORGEJO_TOKEN` set) to also push and create the Forgejo release with source and
|
```bash
|
||||||
toolchain tarballs. The tag doubles as the reproducible bootstrap point: the
|
ludic-dev release [major|minor|patch] # omit the level to derive it from the changesets
|
||||||
source archive plus its checked-in seed rebuild that exact toolchain.
|
git push origin main --follow-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
`ludic-dev release` aggregates the pending changesets into a new `CHANGELOG.md` section
|
||||||
|
— grouped by change type, with each changeset's markdown kept intact — bumps
|
||||||
|
`VERSION`, commits `chore(release): vX.Y.Z`, and tags it.
|
||||||
|
|
||||||
|
**Pushing the tag is what publishes.** The `release` workflow builds the
|
||||||
|
toolchain from the IR seed, runs `ludic-dev test`, `ludic-dev test-tools` and `ludic-dev bootstrap-cfree`
|
||||||
|
against the tagged tree, and only then creates the Forgejo release — with the
|
||||||
|
source tarball, a Linux toolchain build, a `.sha256` beside each, and that version's
|
||||||
|
`CHANGELOG.md` section as the notes. It refuses to publish if the tag and
|
||||||
|
`VERSION` disagree or the changelog has no section for it.
|
||||||
|
|
||||||
|
Each toolchain artifact is a complete install root — `bin/` beside `runtime/`,
|
||||||
|
`packages/` and `VERSION` — which is exactly what `install.sh` unpacks into
|
||||||
|
`~/.ludic`. A release with no artifact for a platform is not a broken install
|
||||||
|
there: the installer falls back to bootstrapping from the source tarball's IR
|
||||||
|
seed. But the macOS artifacts are the ones most people get, so attach them.
|
||||||
|
|
||||||
|
macOS artifacts cannot be produced on the Linux runner — a `darwin-arm64` build
|
||||||
|
needs a macOS host, and there is no cross-compile path (it would need the Xcode
|
||||||
|
SDK and a Mach-O linker). Attaching one therefore means either registering a
|
||||||
|
macOS runner and giving it a job, or running the same command CI runs from a
|
||||||
|
Mac. Either way it is `ludic-dev publish`, which only adds assets the release is missing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FORGEJO_TOKEN=… ludic-dev publish v0.4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Checksums are one `.sha256` file per artifact rather than a single `SHA256SUMS`,
|
||||||
|
precisely because a release can be assembled from more than one host and an
|
||||||
|
asset that already exists is never overwritten. Verify one with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shasum -a 256 -c ludic-0.4.0-src.tar.gz.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
The tag doubles as the reproducible bootstrap point: the source archive plus its
|
||||||
|
checked-in seed rebuild that exact toolchain.
|
||||||
|
|
||||||
|
## Where the name and the URLs live
|
||||||
|
|
||||||
|
The language may yet be renamed and the project may yet move hosts, so the
|
||||||
|
things that carry a name are kept few and listed here rather than discovered one
|
||||||
|
broken link at a time. Everything host-shaped has an environment override, so a
|
||||||
|
move can be rehearsed before it is committed.
|
||||||
|
|
||||||
|
**Hosts and URLs.** The install one-liner is served from the documentation site,
|
||||||
|
which publishes `install.sh` beside the pages that quote it (`ludic-dev docs-gen`
|
||||||
|
copies it in; `docs-check` fails without it). Change the host in:
|
||||||
|
|
||||||
|
| Where | What |
|
||||||
|
|---|---|
|
||||||
|
| `install.sh` | `REPO_API`, `REPO_URL`, `INSTALL_URL` — each `${LUDIC_…:-default}`, so `LUDIC_REPO_URL=… sh install.sh` tests a move without editing anything |
|
||||||
|
| `tools/ludic-cli/project.ludic` | `install_url()` (`$LUDIC_INSTALL_URL`), used by `ludic upgrade` and `ludic doctor` |
|
||||||
|
| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic-dev publish` |
|
||||||
|
| `docs/site/site.json` | `repo_url`, the `start.terminal` one-liner, and the doc links in `nav_links` |
|
||||||
|
| Prose | `README.md`, `COMPILING.md`, `tools/editors/README.md`, and the two editor plugins' "server not found" messages |
|
||||||
|
|
||||||
|
**The name itself.** A rename touches, in rough order of blast radius:
|
||||||
|
|
||||||
|
- **The file extension** `.ludic` — the compiler (`strip_ludic`, `do_import`,
|
||||||
|
`is_ludic_file`), every editor asset (`tools/editors/shared/*.json`,
|
||||||
|
`vscode/package.json`, the JetBrains `LudicFileType`), and every source file
|
||||||
|
in the tree.
|
||||||
|
- **The binaries** `ludic`, `ludicc`, `ludic-dev`, `ludic-fmt`, `ludic-lsp` —
|
||||||
|
`cmd_dev_build` in `toolchain.ludic`, the release staging in `release.ludic`,
|
||||||
|
`install.sh`, the editors' executable-name lists. Only the first, third and
|
||||||
|
fourth of those ship: `ludic-dev` is built from a checkout and stays there.
|
||||||
|
- **The install root** `~/.ludic` and the source directories `tools/ludic-cli/`,
|
||||||
|
`tools/ludic-tools/`, `packages/ludic.*`.
|
||||||
|
- **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`,
|
||||||
|
`LUDIC_STORE`, `LUDIC_PKG_PROXY`, `LUDIC_INSTALL_URL`, `LUDIC_KEEP_TMP`,
|
||||||
|
`LUDIC_COVERAGE` — keep the old names working for a release if anyone has them
|
||||||
|
in a script.
|
||||||
|
- **Identifiers that are contracts with other software**: the TextMate scope
|
||||||
|
`source.ludic`, the VS Code language id `ludic`, the JetBrains plugin id
|
||||||
|
`io.ludic.ide`, and the `ludic` code-fence tag understood by the Markdown
|
||||||
|
injection and by `ludic-dev check-docs`.
|
||||||
|
- **The prose**: `README.md`, `LANGUAGE.md`, `COMPILING.md`, `docs/**`, and
|
||||||
|
`docs/site/site.json`'s `brand`/`meta`.
|
||||||
|
|
||||||
|
`ludic-dev test` is the safety net for the mechanical part — it builds the
|
||||||
|
toolchain, stages an install, and runs `new` → `build` → `test` through it, so a
|
||||||
|
half-finished rename fails there rather than in someone's terminal.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|
@ -99,7 +191,7 @@ source archive plus its checked-in seed rebuild that exact toolchain.
|
||||||
| `perf` | a performance improvement |
|
| `perf` | a performance improvement |
|
||||||
| `docs` | documentation only (`docs/`, README, comments) |
|
| `docs` | documentation only (`docs/`, README, comments) |
|
||||||
| `test` | tests only |
|
| `test` | tests only |
|
||||||
| `build` | the build/bootstrap machinery (seed, `bin/x`, linking) |
|
| `build` | the build/bootstrap machinery (seed, `bin/ludic`, linking) |
|
||||||
| `ci` | CI workflows under `.forgejo/` |
|
| `ci` | CI workflows under `.forgejo/` |
|
||||||
| `style` | formatting/whitespace, no behaviour change |
|
| `style` | formatting/whitespace, no behaviour change |
|
||||||
| `chore` | routine housekeeping with no other bucket |
|
| `chore` | routine housekeeping with no other bucket |
|
||||||
|
|
@ -121,7 +213,7 @@ source archive plus its checked-in seed rebuild that exact toolchain.
|
||||||
so a green local commit is a green CI run.
|
so a green local commit is a green CI run.
|
||||||
|
|
||||||
- **Formatting:** `ludic-fmt` is the source of truth (2-space indent, LF, UTF-8);
|
- **Formatting:** `ludic-fmt` is the source of truth (2-space indent, LF, UTF-8);
|
||||||
the repo `.editorconfig` mirrors it. Run `bin/ludic-fmt -w` on files you touch.
|
the repo `.editorconfig` mirrors it. Run `bin/ludic fmt` on files you touch.
|
||||||
The contract CI enforces is *idempotence* — `ludic-fmt` re-run on its own output
|
The contract CI enforces is *idempotence* — `ludic-fmt` re-run on its own output
|
||||||
is a no-op — which leaves deliberate hand alignment in place; it is not a
|
is a no-op — which leaves deliberate hand alignment in place; it is not a
|
||||||
blanket `fmt(x) == x`.
|
blanket `fmt(x) == x`.
|
||||||
|
|
@ -149,11 +241,44 @@ non-destructive version of "tidy the history" without touching a single commit.
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
- Base your branch on `main`.
|
- Base your branch on `main`.
|
||||||
- Ensure `bin/x test` (and `bin/x bootstrap-cfree` for compiler/runtime changes)
|
- Ensure `bin/ludic-dev test` (and `bin/ludic-dev bootstrap-cfree` for compiler/runtime changes)
|
||||||
pass, and that `ludic-fmt` leaves your files unchanged.
|
pass, and that `ludic-fmt` leaves your files unchanged.
|
||||||
- Fill in the PR template checklist. Reference the issue you close with
|
- Fill in the PR template checklist. Reference the issue you close with
|
||||||
`Closes #NN` in the description or a commit message.
|
`Closes #NN` in the description or a commit message.
|
||||||
|
|
||||||
|
## CI (self-hosted runners)
|
||||||
|
|
||||||
|
Every workflow starts by cloning `${{ github.server_url }}/${{ github.repository }}`.
|
||||||
|
On a self-hosted Forgejo runner that URL is usually the instance's *internal*
|
||||||
|
address (e.g. `http://forgejo:3000`), so **the job container must be able to
|
||||||
|
resolve it**. The runner puts each job on a fresh per-job network by default,
|
||||||
|
which the Forgejo container is not attached to — so the clone fails with:
|
||||||
|
|
||||||
|
```
|
||||||
|
fatal: unable to access 'http://forgejo:3000/…': Could not resolve host: forgejo
|
||||||
|
```
|
||||||
|
|
||||||
|
Give the runner a config that pins job containers to a network Forgejo is also
|
||||||
|
on. A dedicated network is better than the general application network, so a CI
|
||||||
|
job cannot reach unrelated services:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# the runner's config.yml, passed with: forgejo-runner daemon --config …
|
||||||
|
container:
|
||||||
|
network: forgejo-ci
|
||||||
|
```
|
||||||
|
|
||||||
|
with `forgejo-ci` attached to the Forgejo container as well. Verify it without
|
||||||
|
running a workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network forgejo-ci alpine:3 getent hosts forgejo
|
||||||
|
```
|
||||||
|
|
||||||
|
This failure mode is intermittent if left unfixed: Docker forwards names it
|
||||||
|
cannot resolve to the host's resolver, which may answer for the container name
|
||||||
|
often enough that CI looks healthy for a while.
|
||||||
|
|
||||||
## Reporting issues
|
## Reporting issues
|
||||||
|
|
||||||
Use the templates under [`.forgejo/issue_template/`](.forgejo/issue_template):
|
Use the templates under [`.forgejo/issue_template/`](.forgejo/issue_template):
|
||||||
|
|
|
||||||
670
EVENTS-DESIGN.md
670
EVENTS-DESIGN.md
|
|
@ -1,670 +0,0 @@
|
||||||
# Events & modding, expanded — a design doc
|
|
||||||
|
|
||||||
> **Status: EV0 fully shipped; EV1 (spawn/despawn), EV2 (first cut) and EV3
|
|
||||||
> shipped; EV4–EV7 are design.** Implemented, self-hosted to the C-free fixpoint,
|
|
||||||
> and each a `bin/x test` check:
|
|
||||||
> - **EV0** — `event`/`@On`/`emit` lowered to `@ev_<E>` dispatch (compile-time
|
|
||||||
> listeners), **plus the foreign C ABI** (`ludic_on_<E>`, the `%Ev_<E>` payload
|
|
||||||
> struct, a fixed-capacity listener array), proven by a C mod in
|
|
||||||
> [`tests/mod_c/mod.c`](tests/mod_c/mod.c) binding
|
|
||||||
> [`examples/mod_host.ludic`](examples/mod_host.ludic). Byte-identical when no
|
|
||||||
> event is declared. ([`examples/events/events.ludic`](examples/events/events.ludic))
|
|
||||||
> - **EV1** — public events across the **whole architecture**, every scope shipped:
|
|
||||||
> **program** (`@Public @OnStart`/`@OnQuit` → `program_start`/`program_quit`,
|
|
||||||
> [`examples/events/program_events.ludic`](examples/events/program_events.ludic)); **models**
|
|
||||||
> (`@Public @OnSpawn`/`@OnDespawn` → `model_<M>_spawn`/`_despawn`,
|
|
||||||
> [`examples/events/promote.ludic`](examples/events/promote.ludic)); **properties** (`@Public
|
|
||||||
> @OnAttach`/`@OnDetach`/`@OnEnable`/`@OnDisable` → `prop_<P>_attach` etc.,
|
|
||||||
> [`examples/events/prop_events.ludic`](examples/events/prop_events.ludic)); **scenes** (a
|
|
||||||
> `public` scene → `scene_<S>_enter`/`_exit`,
|
|
||||||
> [`examples/events/scene_events.ludic`](examples/events/scene_events.ludic)); and **layers** (a
|
|
||||||
> `public` layer + `enable/disable layer L` → `layer_<L>_show`/`_hide`,
|
|
||||||
> [`examples/events/layer_events.ludic`](examples/events/layer_events.ludic)) — which also
|
|
||||||
> landed **SCENES E2 layer toggle** (`@LE_<L>` flag gating a layer's handlers).
|
|
||||||
> - **EV2 / EV2b** — the world table: the reflection ABI, generated from the
|
|
||||||
> compile-time schema, so a mod reads, writes, scans, identifies, **and creates**
|
|
||||||
> entity state **by name** without compiling against the game. `ludic_prop_id` /
|
|
||||||
> `ludic_field_id` / `ludic_get` / `ludic_set` / `ludic_has` (read/write —
|
|
||||||
> [`world_mod.c`](tests/mod_c/world_mod.c)); `ludic_entity_count` / `ludic_kind` /
|
|
||||||
> `ludic_model_id` (scan and identify — [`world_scan.c`](tests/mod_c/world_scan.c));
|
|
||||||
> `ludic_spawn(model_id)` (create, reusing the compiler's own spawn lowering —
|
|
||||||
> [`world_spawn.c`](tests/mod_c/world_spawn.c)); `get`/`set` address each field by
|
|
||||||
> its real struct offset, correct for `int`/`fixed`/`byte`/`ptr` and mixed layouts
|
|
||||||
> ([`world_mixed.c`](tests/mod_c/world_mixed.c)); and iterate
|
|
||||||
> (`ludic_query_next`, [`world_query.c`](tests/mod_c/world_query.c)). Emitted only
|
|
||||||
> for an ECS program that declares events, so event-free games stay byte-exact.
|
|
||||||
> The world table is complete: read, write, scan, identify, create, iterate.
|
|
||||||
> - **EV3** — `cancellable` events, the `cancel` verb, and `emit E(…)` as an
|
|
||||||
> expression returning the veto flag. ([`examples/events/cancel.ludic`](examples/events/cancel.ludic))
|
|
||||||
> - **EV5** — leak-proof scoped listeners: `ludic_off_<E>(token)` (explicit
|
|
||||||
> unregister; dispatch skips tombstoned slots), `ludic_on_entity_<E>(entity, cb)`
|
|
||||||
> (entity-scoped), and a generated `ludic_sweep_entity` called from `despawn` that
|
|
||||||
> nulls every listener the dying entity owned — a listener can't leak past its
|
|
||||||
> entity. Proven by [`tests/mod_c/scoped_mod.c`](tests/mod_c/scoped_mod.c).
|
|
||||||
> - **EV6** — re-entrant `emit` is depth-bounded (`@ev_depth` vs `EV_DEPTH_CAP`): a
|
|
||||||
> listener may emit another event, but an event cycle traps as an early return
|
|
||||||
> instead of hanging the frame. Dispatch order was already deterministic (array,
|
|
||||||
> registration order). Proven by [`examples/events/recurse.ludic`](examples/events/recurse.ludic).
|
|
||||||
>
|
|
||||||
> - **EV7 (schema opening)** — a mod defines a brand-new component at runtime:
|
|
||||||
> `ludic_register_prop(name, nfields)` mallocs flat `[MAX_ENT × nfields × i32]`
|
|
||||||
> storage + a has-flag array and returns a prop id past the compile-time range;
|
|
||||||
> `ludic_attach_dyn`/`ludic_detach_dyn` toggle it on an entity; `get`/`set`/`has`/
|
|
||||||
> `prop_id` fall through to the dynamic registry for ids ≥ the compile-time count.
|
|
||||||
> A mod adds entirely new data to entities by name, with per-entity isolation.
|
|
||||||
> Proven by [`tests/mod_c/world_dyn.c`](tests/mod_c/world_dyn.c). (EV7's other
|
|
||||||
> half — networking's local/remote event split — has no substrate in Ludic yet.)
|
|
||||||
>
|
|
||||||
> Still design: EV4 (the scripting-shim bridge — deferred to keep the suite
|
|
||||||
> interpreter-free) and EV7 networking. This is a companion to
|
|
||||||
> [LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md) and [SCENES-DESIGN.md](SCENES-DESIGN.md).
|
|
||||||
> Where those docs extend Ludic's *internal, compile-time* lifecycle, this one
|
|
||||||
> proposes the *external, runtime* layer that turns those same lifecycle moments
|
|
||||||
> into a public event surface — the foundation a game can hand to mods written in
|
|
||||||
> Ludic, JS/TS, Lua, or anything with a C ABI. It distills a survey of modding and
|
|
||||||
> event systems (§3) into a phased roadmap (EV0–EV7, §12–§13). §14 lists the open
|
|
||||||
> decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Thesis
|
|
||||||
|
|
||||||
Ludic already has a lifecycle. `@OnSpawn(Enemy)`, `@OnDetach(Sprite)`, scene
|
|
||||||
`on enter`, `@OnDespawn(M, reason: r)` — every one is a **compile-time,
|
|
||||||
closed-world, zero-cost** hook that desugars to a direct call at a fixed site.
|
|
||||||
That is the right design for the *game author*, who is compiled together with the
|
|
||||||
game. It is exactly the wrong design for a *mod author*, who is not.
|
|
||||||
|
|
||||||
A modding event system is the mirror image of the lifecycle layer along three axes:
|
|
||||||
|
|
||||||
| | Lifecycle hooks (today) | Modding events (this doc) |
|
|
||||||
|---|---|---|
|
|
||||||
| World | **closed** — all handlers known at compile time | **open** — mods add listeners after compilation |
|
|
||||||
| Binding | **static** — a checked symbol, a direct call | **dynamic** — registered at load, dispatched at runtime |
|
|
||||||
| Language | **in-language** — Ludic, compiled together | **cross-language** — JS/TS/Lua/native over an ABI |
|
|
||||||
|
|
||||||
The instinct would be to build a second, parallel system. **The design that keeps
|
|
||||||
Ludic's discipline builds one system seen from two sides.** A lifecycle hook is a
|
|
||||||
*private* view of a moment; a public event is the *same moment* exposed across the
|
|
||||||
ABI. The author promotes a hook to an event; the compiler keeps its zero-cost
|
|
||||||
direct calls **and** emits one guarded `bus_emit` at the very same site. Nothing
|
|
||||||
exposed → nothing emitted → goldens stay byte-identical, exactly like `has_ecs`
|
|
||||||
and the `g_ondespawn` shutdown walk.
|
|
||||||
|
|
||||||
**The Luanti dividend.** The gap analysis (`LUANTI-ROADMAP.md`) found that ~57k of
|
|
||||||
Luanti's lines exist only to bridge C++ and Lua, and that its mod predicates are
|
|
||||||
*runtime strings* it must re-interpret every call. Ludic pays neither tax. The
|
|
||||||
reflection surface a mod needs — "what properties exist, what fields, at what
|
|
||||||
offsets" — is a **compile-time fact**; the compiler can *generate* the bridge
|
|
||||||
instead of a human hand-writing 57k lines, and it is always in sync with the game
|
|
||||||
it describes. A mod itself written in Ludic and compiled to a shared library binds
|
|
||||||
that surface with **zero marshalling**; a Lua mod binds the same surface through
|
|
||||||
its FFI. One ABI, every language.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. What Ludic has today, and why it can't reach a mod
|
|
||||||
|
|
||||||
The lifecycle table from [LIFECYCLE-DESIGN.md §2](LIFECYCLE-DESIGN.md), every cell
|
|
||||||
filled, every cell a zero-cost desugar:
|
|
||||||
|
|
||||||
| Scope | Setup hook | Teardown hook | Fire site the compiler already owns |
|
|
||||||
|---|---|---|---|
|
|
||||||
| program | `@OnStart` | `@OnQuit` | boot / shutdown |
|
|
||||||
| entity | `@OnSpawn(M)` | `@OnDespawn(M, reason)` | `spawn` / `despawn` / shutdown-walk |
|
|
||||||
| property (structural) | `@OnAttach(P)` | `@OnDetach(P)` | `attach` / `detach` |
|
|
||||||
| property (toggle) | `@OnEnable(P)` | `@OnDisable(P)` | `enable` / `disable` |
|
|
||||||
| scene | `on enter` | `on exit` | `become` (and `push`/`pop`, SCENES E3) |
|
|
||||||
|
|
||||||
Two more fire sites are proposed but unbuilt, and both are natural events:
|
|
||||||
`@OnChange(P)` (LC2 — a value-change hook the compiler can emit right after every
|
|
||||||
write site) and `@OnStartMatch`/`@OnStopMatch` (LC3 — query-membership edges).
|
|
||||||
|
|
||||||
Every one of these is a place the compiler **already writes a call**. The problem
|
|
||||||
is purely that the call is *closed*: its targets are fixed at compile time, so a
|
|
||||||
mod loaded at runtime has no way to be one of them. The entire job of this doc is
|
|
||||||
to add, at each of these sites, an **opt-in second exit** to an open runtime list —
|
|
||||||
without touching the closed path's cost when no one opts in.
|
|
||||||
|
|
||||||
What a mod additionally needs, that no hook provides:
|
|
||||||
|
|
||||||
- a **stable name** for each event that survives recompilation (a mod compiled
|
|
||||||
against v1 must still bind in v1.1);
|
|
||||||
- a way to **read and write game state** it did not compile against (the world
|
|
||||||
table, §9);
|
|
||||||
- a way to **veto or rewrite** an action before it commits, not just observe it
|
|
||||||
after (cancellable events, §8);
|
|
||||||
- a **loader** — mods enable, disable, and unload, and their listeners must vanish
|
|
||||||
cleanly when they do (§10).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Research digest — the one idea to steal from each
|
|
||||||
|
|
||||||
The lifecycle doc surveyed engines for *internal* lifecycle. This surveys systems
|
|
||||||
for their *modding and event* surface — how untrusted, separately-authored code
|
|
||||||
plugs into a running game.
|
|
||||||
|
|
||||||
| System | The transferable idea |
|
|
||||||
|---|---|
|
|
||||||
| **Bukkit / Spigot** (Minecraft) | The canonical **cancellable event**: `Cancellable.setCancelled(true)` vetoes the action; `EventPriority` orders listeners; `@EventHandler(ignoreCancelled=true)` opts out of already-vetoed events. Events are *classes*, checked at bind time — not strings. |
|
|
||||||
| **Fabric** (Minecraft) | `Event<T>` backed by an **invoker over a plain array** of callbacks — deterministic registration order, no reflection at dispatch, phases for ordering. The closest existing design to what Ludic wants: fast, ordered, array-backed. |
|
|
||||||
| **Factorio** | **Deterministic** modded events for multiplayer lockstep: `script.on_event(defines.events.X)`, numeric event ids, `raise_event` for custom events, **filtered** subscriptions. Proof that a heavily-modded game can still replay bit-for-bit. |
|
|
||||||
| **Minetest / Luanti** | `register_on_*` + a string-keyed global (`minetest.*`) world API. The thing to beat: its predicates are runtime strings, and its C++↔Lua bridge is 57k hand-written lines. |
|
|
||||||
| **Godot** | **Signals as a first-class language construct**: `signal hurt(amount)`, `emit_signal`, `connect`. Decoupled, per-object, declared where the data lives. |
|
|
||||||
| **DOM events** | The **two-phase dispatch** vocabulary: capture → target → bubble, `preventDefault` (veto the default action) vs `stopPropagation` (halt the chain), and *passive* listeners that promise not to cancel (so dispatch can skip the veto check). |
|
|
||||||
| **Node `EventEmitter`** | The dead-simple baseline `on`/`emit` — and its footguns: untyped string names (a typo silently never fires) and **listener leaks** (a listener on a dead object keeps it alive). Design both out. |
|
|
||||||
| **flecs / Bevy observers** | **ECS-native reactive events**: an event *targeted at an entity*, observers that fire on component add/set/remove, deferred so mutation-during-iteration is safe. The correct shape for an ECS. |
|
|
||||||
| **Blender `bpy.app.handlers`** | Named application-level handler lists a script appends to, with a `persistent` flag controlling survival across file loads — the "engine lifecycle exposed to scripts" model, and the lesson that *survival scope* must be explicit. |
|
|
||||||
| **Roblox** | `BindableEvent` (local) vs `RemoteEvent` (across the network boundary) — the same event abstraction, one flag deciding whether it crosses a trust/latency boundary. Relevant the day Ludic has networking. |
|
|
||||||
|
|
||||||
Five **footguns** the survey warns against, to design *out* of Ludic from the start:
|
|
||||||
|
|
||||||
1. **Untyped string events.** Node/DOM let any string be an event; a typo never
|
|
||||||
fires and never errors. Ludic's core events are compiler-checked symbols; only
|
|
||||||
genuinely-dynamic *mod-defined* events use interned strings, and those must be
|
|
||||||
*registered* before use (§6), so an unknown name is a load-time error, not a
|
|
||||||
silent no-op.
|
|
||||||
2. **Listener leaks.** A listener bound to an entity that despawns must die with
|
|
||||||
it. Ludic ties listener lifetime to the scope it names (§10) — entity-scoped
|
|
||||||
listeners are swept by the same despawn walk that already runs.
|
|
||||||
3. **Nondeterministic dispatch order.** Hash-map iteration over listeners breaks
|
|
||||||
replay and save-load. Ludic dispatches in a **defined order** (priority, then
|
|
||||||
registration order) so a modded game stays deterministic — a hard constraint,
|
|
||||||
not a nicety, given Ludic's deterministic-by-design rng and byte-identical
|
|
||||||
goldens.
|
|
||||||
4. **Re-entrancy / mutate-during-dispatch.** A listener that emits another event,
|
|
||||||
or despawns the entity mid-dispatch, is the flecs "command during iteration"
|
|
||||||
hazard. Ludic defers structural changes made inside dispatch to the next sync
|
|
||||||
point (ties to LIFECYCLE LC5), and bounds re-entrant emit depth.
|
|
||||||
5. **Cancellation ambiguity.** If two listeners disagree, who wins? Ludic's rule
|
|
||||||
(§8): **one veto wins and is sticky**; later listeners see the cancelled state
|
|
||||||
and, unless they opted into `ignoreCancelled`, are skipped.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. The two layers, named
|
|
||||||
|
|
||||||
To talk about this precisely the doc fixes two words:
|
|
||||||
|
|
||||||
- A **hook** is the existing compile-time construct: an `@`-annotation or scene
|
|
||||||
clause that desugars to a direct call. Closed, zero-cost, author-only. Unchanged.
|
|
||||||
- An **event** is the new runtime construct: a named, ABI-visible moment that any
|
|
||||||
registered listener — in any language — may observe or (if cancellable) veto.
|
|
||||||
|
|
||||||
An event is *fed by* a hook site. Promoting is additive: the hook keeps firing its
|
|
||||||
compile-time listeners as direct calls; the event is an extra, guarded emission at
|
|
||||||
the same site. **Author code never pays for the bus it doesn't expose, and mod
|
|
||||||
code never sees a hook it wasn't given.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. EV0 — the event bus core
|
|
||||||
|
|
||||||
The minimum viable layer: declare an event, emit it, and have both in-language and
|
|
||||||
foreign listeners receive it — with zero cost when a program declares no events.
|
|
||||||
|
|
||||||
**Declaring a custom event.** A first-class declaration, mirroring `property`:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
event PlayerHurt { entity: int, amount: int } # a payload is a flat POD record
|
|
||||||
event WaveCleared { } # payloads may be empty
|
|
||||||
```
|
|
||||||
|
|
||||||
**Emitting.** A statement, mirroring `spawn`/`emit_signal`:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
emit PlayerHurt(entity: e, amount: dmg)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Listening in-language** (author code, or a *native* Ludic mod) reuses the
|
|
||||||
annotation channel, mirroring `@OnSpawn`:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
@On(PlayerHurt) handler FlashRed { hud_flash(0xFF0000) }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Listening across the ABI** (a JS/TS/Lua mod) goes through the stable C ABI:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* the entire foreign-facing event ABI — four functions */
|
|
||||||
uint32_t ludic_event_id(const char *name); /* intern → stable id */
|
|
||||||
uint32_t ludic_on(uint32_t event, int32_t prio, ludic_cb cb, void *ctx);
|
|
||||||
void ludic_off(uint32_t token);
|
|
||||||
void ludic_emit(uint32_t event, void *payload); /* mod-raised events */
|
|
||||||
/* cb: void (*)(void *ctx, void *payload) — payload is the flat POD record */
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lowering — the discipline holds.** An exposed event's emit site becomes:
|
|
||||||
|
|
||||||
```
|
|
||||||
; emit PlayerHurt(entity: e, amount: dmg) lowers to:
|
|
||||||
1. build the payload record on the stack (POD, no heap)
|
|
||||||
2. call each compile-time @On(PlayerHurt) handler directly ; zero-cost path
|
|
||||||
3. if g_listeners[EV_PlayerHurt].count != 0: ; one branch
|
|
||||||
loop the runtime listener list, calling each cb(ctx, &payload)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **A program that declares no `event` emits none of this.** A `has_events` flag
|
|
||||||
(exactly like `has_ecs`, `g_ondespawn`) gates the whole subsystem; a game with no
|
|
||||||
public events is byte-for-byte identical to today. This is the non-negotiable
|
|
||||||
invariant every phase preserves.
|
|
||||||
- The compile-time `@On` handlers are direct calls appended to the site — a native
|
|
||||||
listener costs the same as a lifecycle hook. Only *foreign* listeners walk the
|
|
||||||
runtime list, and an event with zero foreign listeners is a single count check.
|
|
||||||
- The runtime list is a **compiler-owned, fixed-capacity buffer** per event
|
|
||||||
(like the scene stack in SCENES E3) — not heap, not a hash map. `ludic_on` is an
|
|
||||||
index bump; `ludic_off` tombstones a slot. Deterministic order falls out of the
|
|
||||||
array (§7 of SCENES' "no dispatch tables" spirit, honestly bent — see §11).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. EV1 — promoting hooks to events (the taxonomy)
|
|
||||||
|
|
||||||
Custom `event`s (EV0) cover author-raised signals. The **lifecycle** events —
|
|
||||||
spawn, despawn, attach, scene enter — should not require the author to hand-write
|
|
||||||
an `emit` in every `@OnSpawn`. Instead, a hook is promoted with one annotation:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
@Public @OnSpawn(Enemy) handler Init { Health.hp = Health.max }
|
|
||||||
# now firing this hook ALSO emits the public event model.Enemy.spawn
|
|
||||||
```
|
|
||||||
|
|
||||||
`@Public` on a lifecycle hook tells the compiler to add the guarded `bus_emit` at
|
|
||||||
that hook's existing site, with a **generated payload** built from what the hook
|
|
||||||
already binds (the entity id, the model/property fields, the `EndReason`). The
|
|
||||||
result is a uniform event namespace across the whole architecture — precisely the
|
|
||||||
"events for properties, models, scenes, layers, game" the request asks for:
|
|
||||||
|
|
||||||
| Scope | Public event name | Payload | Fed by |
|
|
||||||
|---|---|---|---|
|
|
||||||
| program | `program.start` / `program.quit` | `{}` | `@OnStart` / `@OnQuit` |
|
|
||||||
| phase | `phase.<Name>.pre` / `.post` | `{ frame }` | the phase scheduler |
|
|
||||||
| model | `model.<M>.spawn` / `.despawn` | `{ entity, reason? }` | `@OnSpawn` / `@OnDespawn` |
|
|
||||||
| property (structural) | `prop.<P>.attach` / `.detach` | `{ entity, <fields> }` | `@OnAttach` / `@OnDetach` |
|
|
||||||
| property (toggle) | `prop.<P>.enable` / `.disable` | `{ entity }` | `@OnEnable` / `@OnDisable` |
|
|
||||||
| property (value) | `prop.<P>.change` | `{ entity, field, old, new }` | `@OnChange` (LC2) |
|
|
||||||
| query (membership) | `query.<Q>.enter` / `.exit` | `{ entity }` | `@OnStartMatch`/`@OnStopMatch` (LC3) |
|
|
||||||
| scene | `scene.<S>.enter` / `.exit` / `.push` / `.pop` | `{}` | `on enter`/`on exit`, `push`/`pop` |
|
|
||||||
| layer | `layer.<L>.show` / `.hide` | `{}` | layer toggle (SCENES E2) |
|
|
||||||
|
|
||||||
- **Names are stable strings, ids are fast integers.** `model.Enemy.spawn` is the
|
|
||||||
public contract; the compiler assigns it a numeric id and registers the mapping
|
|
||||||
in a generated init. A mod compiled against the string binds by id at load — so
|
|
||||||
reordering declarations doesn't break a shipped mod (unlike raw
|
|
||||||
decl-order numbering, which is fine for the *closed* scene machine but wrong for
|
|
||||||
an *open* ABI).
|
|
||||||
- **Opt-in per hook, not global.** Only `@Public` hooks emit. A game exposes the
|
|
||||||
slice of its lifecycle it wants moddable and pays for nothing else.
|
|
||||||
- **`@Public` composes with everything.** A `@Public @OnDespawn(Enemy, reason: r)`
|
|
||||||
emits `model.Enemy.despawn` with the `EndReason` in the payload — mods can tell a
|
|
||||||
scene-exit death from a real one, the LC1 dividend extended to the mod boundary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. EV2 — the world table (reflection for mods)
|
|
||||||
|
|
||||||
The user's "game table": the stable, versioned surface a mod uses to **read and
|
|
||||||
write game state it never compiled against**. Minetest's `minetest.*`, Factorio's
|
|
||||||
`game.*`, but *generated* rather than hand-written.
|
|
||||||
|
|
||||||
Because Ludic's data is packed POD in `@S_` arrays whose layout the compiler knows
|
|
||||||
exactly, the compiler can emit a **schema** (property id → field ids → offset +
|
|
||||||
type) plus a small accessor ABI over it:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* the world table — reflection + mutation over the live ECS */
|
|
||||||
uint32_t ludic_prop_id(const char *name); /* "Health" → id */
|
|
||||||
uint32_t ludic_field_id(uint32_t prop, const char *name); /* ("Health","hp")→id */
|
|
||||||
int64_t ludic_get(int32_t entity, uint32_t prop, uint32_t field);
|
|
||||||
void ludic_set(int32_t entity, uint32_t prop, uint32_t field, int64_t v);
|
|
||||||
bool ludic_has(int32_t entity, uint32_t prop);
|
|
||||||
int32_t ludic_spawn(uint32_t model); /* → entity */
|
|
||||||
void ludic_despawn(int32_t entity);
|
|
||||||
uint32_t ludic_query(uint32_t *props, int n); /* → iterator handle */
|
|
||||||
int32_t ludic_query_next(uint32_t iter); /* → entity or -1 */
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Generated from the compile-time schema, so it never drifts.** Add a field to
|
|
||||||
`Health`, recompile, and the schema updates; a mod that asked for
|
|
||||||
`("Health","hp")` still resolves. This is the entire Luanti bridge, minus the
|
|
||||||
hand-written 57k lines and minus the runtime-string re-interpretation.
|
|
||||||
- **`ludic_set` respects the semantic layer.** Writing a field routes through the
|
|
||||||
same path a native write does, so `@OnChange`/`prop.change` (LC2) fires for a
|
|
||||||
mod's write exactly as for the author's — mods can't silently corrupt invariants
|
|
||||||
that hooks are meant to maintain.
|
|
||||||
- **Mods can register content, within limits.** A mod may `ludic_on` existing
|
|
||||||
events and `ludic_emit` custom ones; **defining a new `property`/`model` is a
|
|
||||||
harder call** (it needs storage the closed `@S_` arrays didn't reserve). The
|
|
||||||
pragmatic first cut: models and properties are closed (author-defined), and mods
|
|
||||||
extend *behavior* (listeners, custom events, world reads/writes) but not the
|
|
||||||
*schema*. Opening the schema to mods is EV-late (§13, open decision 4).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. EV3 — cancellable and mutable events
|
|
||||||
|
|
||||||
Observation alone (Node, Blender) can't stop a mod from turning damage off — the
|
|
||||||
modding headline is that a listener runs **before** the action and can veto or
|
|
||||||
rewrite it. Events split into two kinds, distinguished at declaration:
|
|
||||||
|
|
||||||
- **notifications** — fired *after* the fact, observe-only, can't change anything.
|
|
||||||
Cheap, un-ordered-safe, the default. `model.Enemy.spawn` after the spawn.
|
|
||||||
- **decisions** — fired *before* the action, listeners may **cancel** it or
|
|
||||||
**mutate** the payload; the caller reads the verdict and branches. Marked
|
|
||||||
`cancellable` (Bukkit `Cancellable`, DOM `preventDefault`).
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
event cancellable BeforeHurt { entity: int, amount: int } # a decision event
|
|
||||||
|
|
||||||
# an author (or native mod) listener that halves fire damage and vetoes lethal hits:
|
|
||||||
@On(BeforeHurt, prio: 100) handler Armor {
|
|
||||||
BeforeHurt.amount = BeforeHurt.amount / 2 # mutate the payload…
|
|
||||||
if BeforeHurt.amount >= Health.hp { cancel } # …or veto the whole action
|
|
||||||
}
|
|
||||||
|
|
||||||
# the fire site consults the verdict:
|
|
||||||
let dmg = emit? BeforeHurt(entity: e, amount: raw) # emit? returns the (maybe-mutated) payload
|
|
||||||
if !cancelled(dmg) { Health.hp -= dmg.amount }
|
|
||||||
```
|
|
||||||
|
|
||||||
Rules, chosen from the survey to remove the ambiguity footgun:
|
|
||||||
|
|
||||||
- **Priority, then registration order.** `prio:` (default 0) orders listeners
|
|
||||||
high-to-low; ties break by registration order. Deterministic, replay-safe.
|
|
||||||
- **One veto wins and is sticky.** Once a listener calls `cancel`, the event is
|
|
||||||
cancelled for the rest of the chain; later listeners still run (so they can react
|
|
||||||
to the cancellation) unless declared `ignoreCancelled`, which skips them.
|
|
||||||
- **`stopPropagation` is separate from `cancel`.** DOM's distinction: `cancel`
|
|
||||||
vetoes the *action*, `halt` stops the *chain*. Keep both; they answer different
|
|
||||||
questions.
|
|
||||||
- **Passive listeners.** A listener declared `@On(E, passive)` promises not to
|
|
||||||
cancel or mutate — the dispatcher can call it after the decision is settled, and
|
|
||||||
a foreign listener that lies is a load-time capability error (§10), not a
|
|
||||||
mid-frame surprise.
|
|
||||||
- **Mutation is bounded to the payload.** A decision listener rewrites *the payload
|
|
||||||
record*, never arbitrary world state, so the caller's branch is the only place
|
|
||||||
the change takes effect — no spooky action at a distance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. EV4 — the mod ABI & the language-agnostic bridge
|
|
||||||
|
|
||||||
"Agnostic JS/TS/Lua or their own" resolves cleanly once EV0–EV3 exist, because the
|
|
||||||
contract is **the C ABI, not any one language.** Two mod tiers bind the *same* four
|
|
||||||
event functions (§5) and the same world table (§7):
|
|
||||||
|
|
||||||
**Tier 1 — native mods (Ludic → shared library).** A mod is a `.ludic` file
|
|
||||||
compiled to a `.dylib`/`.so`/`.wasm` with `extern fn` bindings
|
|
||||||
([LANGUAGE.md §Functions & FFI](LANGUAGE.md)). It binds the ABI with **zero
|
|
||||||
marshalling** — payloads are the same POD records the host builds — and its `@On`
|
|
||||||
handlers can even be *inlined by the same compiler* if the mod is compiled with the
|
|
||||||
game. This is the tier Luanti can't offer and the one that makes Ludic's modding
|
|
||||||
fast: a compiled predicate where Luanti has a re-interpreted string.
|
|
||||||
|
|
||||||
**Tier 2 — scripted mods (JS/TS/Lua/…).** The game embeds a scripting runtime
|
|
||||||
(QuickJS, Lua, Wasm) and registers a thin per-language shim that:
|
|
||||||
|
|
||||||
1. calls `ludic_event_id("model.Enemy.spawn")` once at load to resolve the id;
|
|
||||||
2. calls `ludic_on(id, prio, trampoline, script_fn)` where `trampoline` is a
|
|
||||||
single C function that marshals the POD payload into the script runtime's values
|
|
||||||
and invokes `script_fn`;
|
|
||||||
3. exposes the world table (§7) as idiomatic bindings (`world.get(e, "Health",
|
|
||||||
"hp")` in Lua, `world.get(e, "Health", "hp")` in TS).
|
|
||||||
|
|
||||||
The host writes **one trampoline per language**, not one per event — the schema
|
|
||||||
(§7) drives the marshalling generically. A Lua mod and a TS mod differ only in
|
|
||||||
their shim; the game core is identical. This is the structural win the Luanti gap
|
|
||||||
analysis pointed at: the bridge cost is *O(languages)*, not *O(events × languages)*
|
|
||||||
hand-written, because the schema is generated.
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────── the stable C ABI ───────────────┐
|
|
||||||
Ludic game core ──────┤ ludic_on / ludic_emit / ludic_get / ludic_set ├────── generated schema
|
|
||||||
(emits at hook sites) └────────────────────┬───────────────────────────┘ (prop→field→offset)
|
|
||||||
│
|
|
||||||
┌────────────────────────────────┼────────────────────────────────┐
|
|
||||||
│ │ │
|
|
||||||
Tier 1: native mod Tier 2: Lua shim Tier 2: JS/TS shim
|
|
||||||
(.dylib, zero marshalling) (one trampoline) (one trampoline)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. EV5 — mod lifecycle, scoping & leak-proofing
|
|
||||||
|
|
||||||
A mod is not eternal; it loads, enables, disables, and unloads, and its listeners
|
|
||||||
must vanish with it — the Node listener-leak footgun, solved structurally.
|
|
||||||
|
|
||||||
- **Every registration returns a token** (`ludic_on → token`), and a mod's tokens
|
|
||||||
are tracked under its **mod handle**. Unloading a mod calls `ludic_off` on all of
|
|
||||||
them at once — a mod can't leak a listener past its own life.
|
|
||||||
- **Listeners may be scoped to a game object.** `ludic_on_entity(entity, …)` binds
|
|
||||||
a listener that the **existing despawn walk** sweeps when that entity dies — the
|
|
||||||
same `@L_despawn_all` loop LC1 already emits, extended to drop entity-scoped
|
|
||||||
listeners. An entity-scoped listener on a dead entity is impossible by
|
|
||||||
construction, not by discipline.
|
|
||||||
- **Scene-scoped listeners** ride SCENES E1: a listener registered while a scene is
|
|
||||||
active is dropped by that scene's synthesized `on exit`, alongside its owned
|
|
||||||
entities. Overlay push/pop (SCENES E3) scopes listeners to the overlay's life.
|
|
||||||
- **Survival is explicit** (Blender's `persistent` lesson): a listener is
|
|
||||||
program-, mod-, scene-, or entity-scoped, chosen at registration. There is no
|
|
||||||
implicit "lives forever" — the default is the narrowest scope that makes sense
|
|
||||||
(mod), and wider survival is opt-in and visible.
|
|
||||||
- **Capabilities gate what a scripted mod may touch** (§14, open decision 6). A mod
|
|
||||||
manifest declares the events and world-table properties it needs; the loader
|
|
||||||
grants ids only for those. A mod that never asked for `Health` cannot `ludic_set`
|
|
||||||
it — an untrusted-code boundary the closed lifecycle layer never needed but an
|
|
||||||
open mod ABI must have.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. EV6 — determinism, re-entrancy & the one honest compromise
|
|
||||||
|
|
||||||
Ludic is deterministic by design — deterministic rng, byte-identical PPM goldens,
|
|
||||||
save-load of the whole World. A modding layer is the classic place that determinism
|
|
||||||
goes to die (hash-ordered listeners, mods reading wall-clock, emit storms). Holding
|
|
||||||
the line is a **feature**, and the same one that makes Factorio's modded multiplayer
|
|
||||||
lockstep-correct.
|
|
||||||
|
|
||||||
- **Dispatch order is total and defined** — priority, then registration order, over
|
|
||||||
an *array*, never a hash map. Two mods loaded in the same order dispatch in the
|
|
||||||
same order on every machine.
|
|
||||||
- **Emit is synchronous by default, deferred on demand.** `emit E` runs listeners
|
|
||||||
now (push-at-the-site, Ludic's natural style — the LIFECYCLE footgun-1 fix).
|
|
||||||
Structural changes a listener requests (spawn/despawn/attach) **defer to the next
|
|
||||||
sync point** (LIFECYCLE LC5's `defer`), so mutate-during-dispatch is safe and
|
|
||||||
batched. Re-entrant `emit` inside a listener is allowed but **depth-bounded** (a
|
|
||||||
compile-time cap, trap on overflow) so an event cycle can't hang a frame.
|
|
||||||
- **Foreign listeners are the determinism boundary.** A native (Tier 1) listener is
|
|
||||||
as deterministic as any handler. A scripted (Tier 2) listener is only as
|
|
||||||
deterministic as the script — so the sandbox (§10) can **deny nondeterministic
|
|
||||||
capabilities** (wall-clock, unseeded rng, filesystem) to a mod that must stay in
|
|
||||||
a deterministic session (multiplayer, replays). Single-player mods can opt out.
|
|
||||||
|
|
||||||
**The one honest compromise.** SCENES-DESIGN's principle is "no dispatch tables —
|
|
||||||
the active-scene path is a register read and a static branch." The runtime
|
|
||||||
listener list *is* a dispatch table, walked at runtime. This doc owns that: it is
|
|
||||||
the **deliberate, opt-in exception**, justified because open-world extension is the
|
|
||||||
entire point of a mod ABI and cannot be resolved at compile time by definition.
|
|
||||||
The mitigations keep it honest — it is (a) gated behind `has_events` so unused it
|
|
||||||
costs nothing, (b) an array not a hash map so it stays deterministic, (c) fed by
|
|
||||||
compile-time-checked names so the *closed* side stays typed, and (d) reached only
|
|
||||||
after the zero-cost direct calls to compile-time `@On` handlers. Ludic pays for a
|
|
||||||
dispatch table exactly when, and only when, a game chooses to be moddable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Lowering summary
|
|
||||||
|
|
||||||
Everything above reduces to constructs Ludic already has or honestly-scoped
|
|
||||||
additions to them:
|
|
||||||
|
|
||||||
| Construct | Lowers to |
|
|
||||||
|---|---|
|
|
||||||
| `event E { … }` | a generated payload record type + a reserved event id + a `has_events` bump |
|
|
||||||
| `emit E(…)` | build POD payload · direct-call each `@On(E)` handler · `if count: walk runtime list` |
|
|
||||||
| `@On(E)` handler | a compile-time listener: a direct call appended to `E`'s emit site (zero-cost) |
|
|
||||||
| `@Public @OnX(…)` | the existing hook's site, plus a guarded `bus_emit` of a payload built from the hook's bindings |
|
|
||||||
| public event name | a stable string interned to an integer id in a generated registry init |
|
|
||||||
| the runtime listener list | a compiler-owned fixed-capacity array per event; `ludic_on` = index bump, `ludic_off` = tombstone |
|
|
||||||
| the world table | a generated schema (prop→field→offset/type) + accessor ABI over the live `@S_` arrays |
|
|
||||||
| `cancellable` / `cancel` | a verdict field on the payload; the emit site branches on it |
|
|
||||||
| entity/scene-scoped listener | dropped by the existing despawn walk / synthesized `on exit` (LC1 / SCENES E1) |
|
|
||||||
| deferred structural change in a listener | LIFECYCLE LC5's `defer` queue, flushed at the sync point |
|
|
||||||
|
|
||||||
No heap for native payloads, no hash map, no per-event hand-written bridge. The
|
|
||||||
active game path is unchanged unless it opts in; the opt-in cost is one branch per
|
|
||||||
exposed event plus the listeners a mod actually registers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Design principles distilled
|
|
||||||
|
|
||||||
1. **One system, two sides.** A public event is a lifecycle hook seen from across
|
|
||||||
the ABI. Don't build a parallel event runtime; promote the sites you already
|
|
||||||
have.
|
|
||||||
2. **Opt-in or invisible.** No `event`, no `@Public` → byte-identical goldens.
|
|
||||||
`has_events` gates the world the way `has_ecs` gates the ECS.
|
|
||||||
3. **Closed stays typed; only the open edge is dynamic.** Core events are
|
|
||||||
compiler-checked symbols; string names exist only at the genuinely-runtime mod
|
|
||||||
boundary, and even there must be registered (no silent typos).
|
|
||||||
4. **Generated bridge, never hand-written.** The world table and payload marshalling
|
|
||||||
come from the compile-time schema, so they never drift and cost O(languages),
|
|
||||||
not O(events × languages). This is the Luanti dividend — spend it.
|
|
||||||
5. **Deterministic dispatch is a feature.** Array order, not hash order; deny
|
|
||||||
nondeterministic capabilities to mods in deterministic sessions. Modded replay
|
|
||||||
and modded multiplayer depend on it.
|
|
||||||
6. **Lifetime follows scope, explicitly.** Every listener names its scope
|
|
||||||
(program/mod/scene/entity); the existing teardown walks sweep it. No implicit
|
|
||||||
immortality, no leaks.
|
|
||||||
7. **One ABI, every language.** The C ABI is the contract. Native mods bind it with
|
|
||||||
zero marshalling; scripted mods bind it through one trampoline per language.
|
|
||||||
Ludic never blesses a single scripting language.
|
|
||||||
8. **Only the semantic layer, still.** Mods observe and decide; they do not get
|
|
||||||
ctor/dtor/move hooks Ludic doesn't have. POD in, POD out.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Suggested implementation order
|
|
||||||
|
|
||||||
Each phase is independently shippable and testable, matching how the repo phases
|
|
||||||
work (and how LIFECYCLE/SCENES sequence).
|
|
||||||
|
|
||||||
- **EV0 — the bus core.** ✅ *Compile-time half shipped.* `event` / `emit` / `@On`
|
|
||||||
with the `g_events`-gated zero-cost lowering: an event compiles to a `@ev_<E>`
|
|
||||||
function whose body is its listeners in declaration order (payload bound by
|
|
||||||
name as params), and `emit E(…)` is a direct call. Verified byte-identical for
|
|
||||||
event-free programs, self-hosted to the C-free fixpoint. Still open in EV0: the
|
|
||||||
foreign C ABI (`ludic_on`/`ludic_emit`) and its runtime listener array, so a
|
|
||||||
mod in another language can join the same dispatch. Implementation notes: AST
|
|
||||||
`N_EVENT`/`S_EMIT`; `parse_event` + `@On` annotation + `emit` statement (guarded
|
|
||||||
by an identifier-lookahead so a bare `emit(...)` call still parses); registries
|
|
||||||
`g_events`/`g_onlisten` (emit_core); `emit_event_fns` (emit_game); `emit_emit`
|
|
||||||
(emit_stmt). [`examples/events/events.ludic`](examples/events/events.ludic) is a `bin/x test` check.
|
|
||||||
- **EV1 — `@Public` hook promotion.** ✅ *All scopes shipped.* `@Public` on a
|
|
||||||
lifecycle hook fires a public event at that hook's site (payload: entity, plus
|
|
||||||
`EndReason` for despawn); `find_event(name)` doubles as the "is this hook
|
|
||||||
public?" gate. Covered: program (`@OnStart`/`@OnQuit` → `program_start`/`_quit`),
|
|
||||||
models (`@OnSpawn`/`@OnDespawn`), properties
|
|
||||||
(`@OnAttach`/`@OnDetach`/`@OnEnable`/`@OnDisable` → `prop_<P>_…`). Scenes and
|
|
||||||
layers use a `public` block modifier instead of an annotation:
|
|
||||||
`scene_<S>_enter`/`_exit` at the synthesized scene functions, and
|
|
||||||
`layer_<L>_show`/`_hide` at the layer-toggle site. Building layer events also
|
|
||||||
delivered **SCENES E2 layer toggle**: `enable/disable layer L` flips an `@LE_<L>`
|
|
||||||
flag that gates that layer's handlers, emitted only for toggled layers so
|
|
||||||
untouched scene programs stay byte-identical.
|
|
||||||
- **EV2 / EV2b — the world table.** ✅ *Read/write/scan/identify/create shipped.*
|
|
||||||
The generated reflection ABI (§7), dispatching a runtime prop/model id to the
|
|
||||||
right `@S_`/`@H_`/`@L_kind` storage: read/write (`prop_id`/`field_id`/`get`/`set`/
|
|
||||||
`has`), scan/identify (`entity_count`/`kind`/`model_id`), and create
|
|
||||||
(`spawn(model_id)`, which reuses the compiler's own spawn lowering — defaults,
|
|
||||||
`@OnSpawn`, and the spawn event). `get`/`set` address each field by its real
|
|
||||||
struct offset (constant struct GEP), correct for `int`/`fixed`/`byte`/`ptr`
|
|
||||||
fields and mixed layouts alike. Emitted only for an ECS program that declares
|
|
||||||
events (gated on `has_ecs() && g_events`), so event-free games are byte-identical.
|
|
||||||
A `ludic_query_next(prop, from)` cursor iterates live entities that have a
|
|
||||||
property. The world table is complete: read, write, scan, identify, create,
|
|
||||||
iterate.
|
|
||||||
- **EV3 — cancellable events.** ✅ *Shipped.* `event cancellable E`, the `cancel`
|
|
||||||
verb, and `emit E(…)` as an expression yielding the veto flag; the flag is a
|
|
||||||
trailing field of `%Ev_<E>`, so a foreign listener vetoes by setting it. Priority
|
|
||||||
ordering and `ignoreCancelled`/`halt` (§8) remain open. The modding headline —
|
|
||||||
observation becomes control.
|
|
||||||
- **EV4 — the scripting bridge.** One reference shim (Lua *or* QuickJS) over the
|
|
||||||
ABI, proving the O(languages) claim end to end.
|
|
||||||
- **EV5 — mod lifecycle & scoping.** ✅ *Shipped.* A parallel owner array `@evO_<E>`
|
|
||||||
(-1 = program-scoped, ≥0 = owning entity); `ludic_on_<E>` and
|
|
||||||
`ludic_on_entity_<E>` register with the right owner; `ludic_off_<E>(token)`
|
|
||||||
tombstones a slot to null and dispatch skips null slots; `ludic_sweep_entity`,
|
|
||||||
called from `emit_despawn` when the program has events, nulls every listener a
|
|
||||||
despawning entity owned. Scene-scoped listeners (drop on `on exit`) remain the
|
|
||||||
same shape applied at the scene teardown — a follow-on.
|
|
||||||
- **EV6 — determinism & re-entrancy.** ✅ *Depth bound shipped.* `@ev_depth`
|
|
||||||
increments on each `@ev_<E>` entry and decrements on exit; past `EV_DEPTH_CAP`
|
|
||||||
(32) a dispatch returns immediately (a cancellable event returns "not
|
|
||||||
cancelled"), so an event cycle can't hang. Dispatch order was already
|
|
||||||
deterministic (array, registration order). Still design: deferred structural
|
|
||||||
changes at a sync point (LC5) and capability gating for deterministic sessions.
|
|
||||||
- **EV7 — schema-opening & networking.** ✅ *Schema-opening shipped.* A mod defines
|
|
||||||
a new component at runtime: `ludic_register_prop(name, nfields)` allocates flat
|
|
||||||
`[MAX_ENT × nfields × i32]` storage + a has-flag array (capacity 32 dynamic
|
|
||||||
components) and returns a prop id past the compile-time range;
|
|
||||||
`ludic_attach_dyn`/`ludic_detach_dyn` toggle presence; `get`/`set`/`has`/`prop_id`
|
|
||||||
fall through to the dynamic registry for a prop id ≥ the compile-time component
|
|
||||||
count. Per-entity storage is isolated (`world_dyn.c`). This is the first genuinely
|
|
||||||
*dynamic* `@S_` storage — a deliberate departure from the closed dense arrays, so
|
|
||||||
it lives entirely behind the ABI (the game's own components stay static and
|
|
||||||
byte-identical). Dynamic components use integer fields addressed by index (no
|
|
||||||
field-name schema). *Still design:* the local/remote event split (Roblox's
|
|
||||||
lesson) waits on Ludic having a networking substrate.
|
|
||||||
|
|
||||||
EV0–EV1 deliver "the whole architecture emits public events." EV2–EV3 are where a
|
|
||||||
mod becomes able to *change the game*. EV4 proves the language-agnostic claim.
|
|
||||||
EV5–EV7 are hardening and reach.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Open decisions
|
|
||||||
|
|
||||||
1. **`emit` verb & payload identity.** Is `emit E(…)` the only spelling, or does a
|
|
||||||
`signal`-style per-property declaration (Godot) read better for the common case?
|
|
||||||
Are payloads always fresh POD records, or can an emit borrow an existing property
|
|
||||||
in place (cheaper, but aliases live storage)?
|
|
||||||
2. **`@Public` granularity.** Per-hook (proposed), per-model (`@Public model
|
|
||||||
Enemy`), or a program-level "expose all lifecycle" switch for prototyping? Does
|
|
||||||
`@Public` belong on the hook or on the `model`/`property`/`scene` it concerns?
|
|
||||||
3. **Name scheme stability.** Dotted strings (`model.Enemy.spawn`) interned to ids —
|
|
||||||
confirmed. Open: are ids stable across recompiles of the *same* source (needed
|
|
||||||
for save-compatibility of a listener table), and how does a renamed model
|
|
||||||
migrate a shipped mod?
|
|
||||||
4. **Schema opening (EV2/EV7).** Do mods stay behavior-only (listeners + custom
|
|
||||||
events + world reads/writes over author-defined schema), or can a mod define new
|
|
||||||
`property`/`model`? The latter needs dynamic `@S_` storage — a real departure
|
|
||||||
from the closed dense arrays (`LUDIC_MAX_ENT 1024`). Probably EV7.
|
|
||||||
5. **Cancellation surface.** Keep `cancel` (veto action) and `halt` (stop chain)
|
|
||||||
distinct (DOM), or collapse to one? Is `ignoreCancelled` per-listener or a
|
|
||||||
priority-band convention?
|
|
||||||
6. **Sandbox model.** Capability manifest per mod (proposed) — at what granularity
|
|
||||||
(per event? per property? per world-table verb)? What is denied by default in a
|
|
||||||
deterministic session, and who declares a session deterministic?
|
|
||||||
7. **Re-entrancy bound.** Compile-time constant emit-depth cap (trap on overflow),
|
|
||||||
or a runtime budget? What is the default depth, and is an event cycle a warning
|
|
||||||
or an error?
|
|
||||||
8. **Scripting runtime, in or out of scope.** Does Ludic *ship* an embedded runtime
|
|
||||||
(QuickJS/Lua) as a blessed default, or only the ABI and reference shims, leaving
|
|
||||||
the runtime to the game? (Bias: ship the ABI + one reference shim; bless no
|
|
||||||
language.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Companion to [LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md) (the hook sites this layer
|
|
||||||
promotes) and [SCENES-DESIGN.md](SCENES-DESIGN.md) (scene/layer/overlay events and
|
|
||||||
scoped-listener teardown). Grounded in the Luanti gap analysis (`LUANTI-ROADMAP.md`):
|
|
||||||
the generated bridge is how Ludic avoids the 57k-line C++↔Lua tax. Supersedes
|
|
||||||
nothing until the compiler work in §12 lands.*
|
|
||||||
2052
LANGUAGE.md
2052
LANGUAGE.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,348 +0,0 @@
|
||||||
# Lifecycle events, expanded — a design doc
|
|
||||||
|
|
||||||
> **Status: LC0–LC1 shipped; LC2–LC6 are design.** The structural attach/detach
|
|
||||||
> pair and `@OnDetach` (§4, LC0), and reason-carrying `@OnDespawn` (§5, LC1), are
|
|
||||||
> implemented and tested ([`examples/lang/detach.ludic`](examples/lang/detach.ludic),
|
|
||||||
> [`examples/lang/reason.ludic`](examples/lang/reason.ludic), `bin/x test` checks). The
|
|
||||||
> extensions LC2–LC6 are research-informed proposals, not built. This document
|
|
||||||
> distills a survey of lifecycle models across seven systems (§3) into a roadmap
|
|
||||||
> for Ludic. §13 lists the open decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Thesis
|
|
||||||
|
|
||||||
A game/ECS usually models lifetime as **create → destroy on a timeline**. A survey
|
|
||||||
of how other systems handle it — Unity (MonoBehaviour + DOTS), Unreal, Bevy,
|
|
||||||
flecs, EnTT, Godot, and non-game paradigms (actor model, declarative UI, RAII) —
|
|
||||||
shows that mature lifecycle designs model something richer than birth and death:
|
|
||||||
|
|
||||||
- **a reaction to a *reason*** — teardown that knows *why* it is ending (Unreal
|
|
||||||
`EndPlay(reason)`, Erlang `terminate(Reason)`, Akka `preRestart(reason, msg)`);
|
|
||||||
- **paired setup/teardown *keyed on dependencies*** — an update is teardown-then-
|
|
||||||
setup on a value change (React `useEffect`, Compose `DisposableEffect`);
|
|
||||||
- **a deterministic consequence of *scope / ownership*** — guaranteed, ordered,
|
|
||||||
single-shot teardown (C++/Rust RAII, DI scoped lifetimes);
|
|
||||||
- **an edge on *query membership*** — fire when data starts/stops matching a
|
|
||||||
composite condition (DOTS `OnStartRunning`, flecs `Monitor`).
|
|
||||||
|
|
||||||
Ludic's model is a good base: lifecycle hooks are `@`-annotations on handlers that
|
|
||||||
**desugar to ordinary code**, firing at fixed timeline moments, keeping the data
|
|
||||||
plain. This doc extends that base along the four axes above **without breaking the
|
|
||||||
desugars-to-code discipline** — every proposal lowers to plain branches and calls,
|
|
||||||
no hidden runtime.
|
|
||||||
|
|
||||||
**One structural advantage worth stating up front.** flecs and EnTT each carry
|
|
||||||
*two* lifecycle layers: a **memory** layer (ctor/dtor/move/copy — because C++
|
|
||||||
objects must be constructed and relocated as archetypes repack) and a **semantic**
|
|
||||||
layer (on_add/on_set/on_remove). Ludic's components are POD in packed `@S_`
|
|
||||||
arrays; there is nothing to construct, destruct, or move-relocate. **Ludic needs
|
|
||||||
only the semantic layer** — half the machinery, none of the "component isn't
|
|
||||||
movable" footguns. Keep it that way.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. What Ludic has today
|
|
||||||
|
|
||||||
Seven hooks, each an annotation that desugars to a handler body at a timeline
|
|
||||||
moment ([LANGUAGE.md §Annotations](LANGUAGE.md)):
|
|
||||||
|
|
||||||
```
|
|
||||||
boot ─ @OnStart ─▶ spawn ─ @OnAttach(P), @OnSpawn(M) ─▶ … ─ @OnDetach(P)/@OnDespawn(M) ─▶ quit ─ @OnQuit
|
|
||||||
```
|
|
||||||
|
|
||||||
The lifecycle reads cleanest as a table of **paired setup/teardown** across five
|
|
||||||
scopes. Every cell is now filled — LC0 closed the one hole (`@OnDetach`):
|
|
||||||
|
|
||||||
| Scope | Setup | Teardown | Driven by |
|
|
||||||
|---|---|---|---|
|
|
||||||
| program | `@OnStart` | `@OnQuit` | boot / quit |
|
|
||||||
| entity | `@OnSpawn(M)` | `@OnDespawn(M)` | `spawn` / `despawn` |
|
|
||||||
| property (structural) | `@OnAttach(P)` | `@OnDetach(P)` ✅ | `attach` / `detach` |
|
|
||||||
| property (toggle) | `@OnEnable(P)` | `@OnDisable(P)` | `enable` / `disable` |
|
|
||||||
| scene | `on enter` | `on exit` | `become` |
|
|
||||||
|
|
||||||
Two things this table already gets right, which the survey flags as the frequent
|
|
||||||
mistakes to avoid:
|
|
||||||
|
|
||||||
- **The toggle pair is distinct from the structural pair.** Unity's clearest
|
|
||||||
lesson is separating the *repeatable* enable/disable cycle (pooling, pausing,
|
|
||||||
data kept) from the *once* create/destroy (data gone). Ludic has both, as
|
|
||||||
distinct verbs: `disable` pauses and keeps data; `detach` structurally removes
|
|
||||||
(a later `attach` re-seeds). This is exactly DOTS enableable-components vs
|
|
||||||
structural add/remove, and Bevy `disabled` vs `Remove`.
|
|
||||||
- **Hooks are typed annotations, not magic-named methods.** MonoBehaviour matches
|
|
||||||
`Awake`/`Update` by *string name* via reflection — a typo silently never runs.
|
|
||||||
Ludic's `@OnSpawn(Enemy)` is a checked reference; a wrong name is a compile
|
|
||||||
error. Preserve this.
|
|
||||||
|
|
||||||
What's missing is everything past "what happened": **why** it happened, **which
|
|
||||||
values changed**, **when composite conditions begin/end to hold**, and
|
|
||||||
**dependency-keyed** setup/teardown. That is the roadmap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Research digest — the one idea to steal from each
|
|
||||||
|
|
||||||
| System | The transferable idea |
|
|
||||||
|---|---|
|
|
||||||
| **Unity MonoBehaviour** | Two-phase init with a global barrier (all `Awake` before any `Start`); repeatable enable-pair vs once create-pair. |
|
|
||||||
| **Unity DOTS** | *Data-driven activation*: `RequireForUpdate` + `OnStartRunning`/`OnStopRunning` — a system edge-triggers when its query starts/stops matching. Enableable components = cheap "logically off." |
|
|
||||||
| **Unreal** | *Reason-carrying teardown*: `EndPlay(EEndPlayReason)` — one teardown, branch on `Destroyed`/`LevelTransition`/`Quit`/…; forces enumerating every death path (no silent deaths). Provenance-tagged construction. |
|
|
||||||
| **Bevy** | Full structural event set Add/Insert/**Replace**/Remove/Despawn with strict order; **Replace exposes the old value before drop**. Hooks (type-level, singular, invariant) vs observers (plural, reactive). Declarative `before`/`after`/`chain` ordering. State `OnEnter`/`OnExit`/`OnTransition`. |
|
|
||||||
| **flecs** | `Monitor` observers fire on *composite query membership* start/stop. Events fire on **real transitions**, not every API call. Deferred-by-default with explicit sync points. |
|
|
||||||
| **EnTT** | `patch` as the *explicit mutation channel* that fires `on_update` (solves "raw writes are invisible"). Opt-in signals — zero cost when unused. |
|
|
||||||
| **Godot** | Tree membership *is* the lifecycle driver; enter top-down, **`_ready` bottom-up** (dependencies initialized first); `queue_free()` deferred safe-delete; `process_mode` pause inherited down the tree. |
|
|
||||||
| **Actor model (OTP/Akka)** | Lifecycle driven by *failure + supervision*: reason-carrying `terminate`, **restart as a state distinct from create/destroy** (stable identity, reset transient state), supervision trees, `code_change` = live state migration. |
|
|
||||||
| **Declarative UI (React/SwiftUI/Compose)** | *Paired setup/teardown keyed on a dependency list* — cleanup co-located with setup so it can't leak; an update **is** keyed teardown-then-setup; lifetime follows *identity*. |
|
|
||||||
| **RAII / Rust `Drop` / DI scopes** | *Scope = lifetime*: deterministic, reverse-construction-order, single-shot, no-resurrection teardown, guaranteed even on early exit; lifetime-mismatch checking (no long-lived thing holding a short-lived handle). |
|
|
||||||
|
|
||||||
Two recurring **footguns** the whole survey warns against, to design *out* of Ludic:
|
|
||||||
|
|
||||||
1. **Silent order-dependent reactivity.** Bevy's removal buffers are cleared at
|
|
||||||
end-of-frame, so a detector that runs before the mutator *misses removals
|
|
||||||
entirely*. If Ludic adds change/removal reactivity, make it either push-based
|
|
||||||
(fire at the mutation site — Ludic's natural style) or loudly order-checked.
|
|
||||||
2. **Invisible in-place writes.** flecs `on_set` and EnTT `on_update` don't fire
|
|
||||||
on a raw pointer write — you must call `modified()`/`patch`. Ludic can dodge
|
|
||||||
this entirely (see LC2): the compiler *sees* every write site.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. LC0 — structural attach/detach + `@OnDetach` ✅ *shipped*
|
|
||||||
|
|
||||||
The one missing cell in §2's table. `attach P on e { overrides }` adds a property
|
|
||||||
to a **live** entity (seeding fields, firing `@OnAttach`); `detach P on e` removes
|
|
||||||
it (firing `@OnDetach`, which reads the outgoing value, before the has-flag
|
|
||||||
clears). Both fire only on a **real transition** (flecs/Bevy idempotent-add
|
|
||||||
semantics): re-attaching a present property or detaching an absent one is a no-op.
|
|
||||||
|
|
||||||
Lowering: `attach` guards on the has-flag and, when absent, reuses the existing
|
|
||||||
`emit_init_component` (seed + `@OnAttach`); `detach` guards on presence, clears the
|
|
||||||
flag, and fires `@OnDetach` with the property bound by name — the same binding the
|
|
||||||
`@OnDisable` path already uses. No new runtime; POD data stays in `@S_` storage.
|
|
||||||
See [`examples/lang/detach.ludic`](examples/lang/detach.ludic).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. LC1 — reason-carrying teardown ✅ *shipped (`@OnDespawn`)*
|
|
||||||
|
|
||||||
The highest-conviction idea in the survey: it appears independently in Unreal
|
|
||||||
(`EndPlay`), Erlang (`terminate`), and Akka (`preRestart`), and Bevy has an open
|
|
||||||
issue asking for it. **Teardown should know *why*.** A destructor frequently needs
|
|
||||||
to branch — save on `Quit` but not on a scene swap, skip network cleanup when the
|
|
||||||
whole program is exiting.
|
|
||||||
|
|
||||||
`@OnDespawn` gains an optional bound **reason**:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
# EndReason { Despawned, SceneExit, Quit } — the compiler owns this enum
|
|
||||||
|
|
||||||
@OnDespawn(Enemy, reason: r) handler Clean {
|
|
||||||
match r {
|
|
||||||
EndReason.Quit => {} # app closing — don't bother dropping loot
|
|
||||||
_ => drop_loot(Health.hp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**What shipped.** The lowering is exactly the cheap desugars-to-code shape the
|
|
||||||
survey promises. The despawn hook compiles to `@on_despawn_<Model>(i32 %e, i32
|
|
||||||
%reason)`; when the hook writes `reason: r`, `r` is bound as an int local reading
|
|
||||||
`%reason`. Each teardown *site* passes a constant `EndReason`:
|
|
||||||
|
|
||||||
- `despawn e` passes `Despawned` (0) — an in-world death.
|
|
||||||
- **program shutdown** passes `Quit` (2): a generated `@L_despawn_all(reason)`
|
|
||||||
walks the live set at `done:` (before `@OnQuit`, matching the timeline) and
|
|
||||||
fires every survivor's `@OnDespawn`. This makes **"no silent deaths"** real —
|
|
||||||
an entity that outlives the run still gets its destructor, and can branch on
|
|
||||||
`Quit` to skip work that only matters mid-game. Emitted only when the program
|
|
||||||
has `@OnDespawn` hooks, so despawn-free programs are byte-for-byte unchanged.
|
|
||||||
- `SceneExit` (1) is reserved: a scene tearing down its owned entities
|
|
||||||
(SCENES-DESIGN E1) will pass it once scene-owned entities land.
|
|
||||||
|
|
||||||
`EndReason` is compiler-owned (resolved in `enum_ordinal`), so `EndReason.Quit`
|
|
||||||
works without a user declaration; a user enum of the same name still shadows it.
|
|
||||||
Backward-compatible: the `reason:` binding is optional, and `@OnDespawn` without
|
|
||||||
it is unchanged. `@OnDetach` and scene `on exit` do **not** yet take reasons
|
|
||||||
(§13.1). See [`examples/lang/reason.ludic`](examples/lang/reason.ludic).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. LC2 — value-change hooks `@OnChange(P)` *(a compile-time win)*
|
|
||||||
|
|
||||||
Every reactive ECS wants "fire when a component's value changes" (flecs `on_set`,
|
|
||||||
EnTT `on_update`, Bevy `Changed<T>`), and every one hits the same footgun: a raw
|
|
||||||
in-place write is invisible, so you must route mutations through a special channel
|
|
||||||
(`modified()`, `patch`) or you miss changes.
|
|
||||||
|
|
||||||
**Ludic can sidestep the footgun because it is an AOT compiler that sees every
|
|
||||||
write site.** A field store `Health.hp = …` is a statement the compiler lowers; if
|
|
||||||
`Health` carries an `@OnChange`, the compiler can emit the hook call *right after
|
|
||||||
the store*. No dirty bits, no end-of-frame flush, no missed-write class of bugs —
|
|
||||||
the thing that is a runtime hazard everywhere else is resolved at compile time.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
@OnChange(Health) handler Bar { hud_set_health(Health.hp) } # after any write to a Health field
|
|
||||||
```
|
|
||||||
|
|
||||||
Open question (§7): fire on *every* write (Bevy's `DerefMut` semantics — simple,
|
|
||||||
may over-fire) or guard with a value compare (fire only on actual change — needs
|
|
||||||
the old value, à la Bevy `Replace`). The compiler has the old value in hand at the
|
|
||||||
store site, so the value-compare form is feasible and is the more useful default.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. LC3 — query-membership edges `@OnStartMatch` / `@OnStopMatch`
|
|
||||||
|
|
||||||
DOTS `OnStartRunning`/`OnStopRunning` and flecs `Monitor` fire when an entity
|
|
||||||
**starts or stops matching a composite query** — not a single component, but a
|
|
||||||
whole condition (`{Position, Velocity, moving}`). This is strictly more expressive
|
|
||||||
than per-property `@OnAttach`, which can't see "the entity now has *both* and is
|
|
||||||
alive." It's the natural ECS form of enter/exit.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
@OnStartMatch(these: [Position, Velocity{dx != 0 or dy != 0}], on: Actor)
|
|
||||||
handler BeginMoving { play("footstep_loop.wav") }
|
|
||||||
|
|
||||||
@OnStopMatch(these: [Position, Velocity{dx != 0 or dy != 0}], on: Actor)
|
|
||||||
handler StopMoving { stop("footstep_loop.wav") }
|
|
||||||
```
|
|
||||||
|
|
||||||
Cost: unlike LC1/LC2 this needs runtime state — a per-entity shadow bit per
|
|
||||||
monitored query ("did it match last tick?"), checked once per frame, edge-
|
|
||||||
triggering the hook on a change. flecs does this by evaluating the query against
|
|
||||||
the entity's previous and current archetype. Ludic would keep a `@M_<query>` bit
|
|
||||||
array parallel to `@H_`. Medium cost; a genuinely differentiated feature.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. LC4 — keyed effects (paired setup/teardown on a dependency list)
|
|
||||||
|
|
||||||
The declarative-UI headline, and the biggest reach. React `useEffect`, Compose
|
|
||||||
`DisposableEffect`, and SwiftUI `.task` all express: *while this thing exists (or
|
|
||||||
while key K holds), set up a resource; when it leaves or K changes, tear it down*
|
|
||||||
— with cleanup **co-located** with setup so it can't leak, and an *update* defined
|
|
||||||
as keyed teardown-then-setup. This collapses create/update/destroy into one
|
|
||||||
primitive.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — sketch
|
|
||||||
@Effect(on: Enemy, keys: [Sprite.id]) handler Body {
|
|
||||||
let tex = image_load(Sprite.id)
|
|
||||||
dispose { image_drop(tex) } # runs on despawn OR when Sprite.id changes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Semantics: the setup runs on spawn (and whenever a listed key changes, after the
|
|
||||||
previous `dispose`), and `dispose` runs on despawn (and before each keyed re-run).
|
|
||||||
It unifies `@OnAttach`/`@OnDetach`/`@OnChange` into one leak-proof unit. Lowering
|
|
||||||
needs somewhere to stash the effect's captured teardown state and last key values
|
|
||||||
per entity — a per-effect side table, re-checked in a phase. Design only; the
|
|
||||||
syntax and storage model are open. This is where Ludic could feel genuinely modern
|
|
||||||
relative to every ECS surveyed (none of which have it).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. LC5 — deferred structural changes with commit points
|
|
||||||
|
|
||||||
DOTS `EntityCommandBuffer`, flecs `defer_begin/end`, and Godot `queue_free()` all
|
|
||||||
make structural change **deferred with an explicit commit point**, so mutating
|
|
||||||
while iterating is safe and batched. Ludic's `spawn`/`despawn` are immediate today,
|
|
||||||
but *already* iteration-safe by a different route — matching is lazy per entity id
|
|
||||||
([LANGUAGE.md](LANGUAGE.md) "Matching is lazy, not snapshotted"), so despawning the
|
|
||||||
current entity is defined. A `defer { … }` block (or `despawn e at LateUpdate`)
|
|
||||||
that queues structural changes to a phase boundary would add batching and a single
|
|
||||||
predictable commit point, and is the prerequisite for safe parallel handlers
|
|
||||||
(the `reads`/`writes` scheduling in SCENES-DESIGN). Design only; lower priority
|
|
||||||
than LC1–LC3 because the immediate path is already safe.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. LC6 — supervision, restart-as-a-state, live migration
|
|
||||||
|
|
||||||
The furthest-out cluster, from the actor model and OTP: lifecycle driven by
|
|
||||||
**failure**, not just create/destroy. Three ideas, all tied to Ludic's eventual
|
|
||||||
hot-reload / bytecode-VM roadmap rather than the near term:
|
|
||||||
|
|
||||||
- **Restart as a distinct state** between create and destroy — preserve an
|
|
||||||
entity's identity, reset its transient components, re-run setup (respawn,
|
|
||||||
hot-reload). Akka's "stable external ref, replaced internal state."
|
|
||||||
- **Supervision / failure escalation** — a subsystem owner declares a policy for
|
|
||||||
child faults (restart one / restart the group / escalate to reload the scene)
|
|
||||||
instead of defensive inline checks. Ludic has no failure model yet, so this
|
|
||||||
waits on one.
|
|
||||||
- **Live state migration** (`code_change`) — a hook that transforms an entity's
|
|
||||||
persistent state across a code/schema version, so hot-reload evolves data
|
|
||||||
instead of destroying it. Directly relevant to a self-hosting language.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Design principles distilled from the footguns
|
|
||||||
|
|
||||||
1. **No silent deaths.** Enumerate every teardown reason (LC1). If the compiler
|
|
||||||
must name the reason at each site, it can't forget a path.
|
|
||||||
2. **Fire on real transitions, not API calls.** Idempotent add/remove — LC0
|
|
||||||
already does this; keep it for every future hook.
|
|
||||||
3. **Keep "paused" and "gone" distinct.** `disable`/`enable` (data kept) vs
|
|
||||||
`detach`/`attach` (structural) — already true; don't let a future feature blur
|
|
||||||
them.
|
|
||||||
4. **Prefer compile-time resolution to runtime tracking.** LC2 turns the
|
|
||||||
universal "invisible write" footgun into a compile-time hook emission because
|
|
||||||
Ludic sees write sites. Reach for this wherever a runtime dirty-bit is the
|
|
||||||
obvious-but-worse option.
|
|
||||||
5. **If reactivity is order-dependent, make it loud.** Never silently drop events
|
|
||||||
at a frame boundary (Bevy's removal-buffer trap). Ludic's push-at-the-site
|
|
||||||
style avoids this by default.
|
|
||||||
6. **Deterministic teardown order.** When a scope tears down many things (a scene
|
|
||||||
unloading its owned entities — SCENES-DESIGN E1), define the order (reverse of
|
|
||||||
creation, RAII-style) rather than leaving it unspecified.
|
|
||||||
7. **Only the semantic layer.** POD components mean no ctor/dtor/move hooks. Don't
|
|
||||||
grow a memory-lifecycle layer Ludic doesn't need.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Suggested implementation order
|
|
||||||
|
|
||||||
- **LC0 — attach/detach + `@OnDetach`.** ✅ Done. Closes the structural pair.
|
|
||||||
- **LC1 — reason-carrying teardown.** ✅ Done for `@OnDespawn` (an `i32 %reason`
|
|
||||||
param + a constant at each site, plus a shutdown despawn-all for `Quit`).
|
|
||||||
`@OnDetach` / `on exit` reasons remain open (§13.1).
|
|
||||||
- **LC2 — `@OnChange(P)`.** Compile-time hook emission at write sites — a
|
|
||||||
Ludic-specific win over every ECS's invisible-write footgun. **Recommended next.**
|
|
||||||
- **LC3 — `@OnStartMatch`/`@OnStopMatch`.** First feature needing runtime shadow
|
|
||||||
state; the expressive ECS enter/exit.
|
|
||||||
- **LC4 — keyed effects.** The modern, leak-proof unification. Design first.
|
|
||||||
- **LC5 — deferred structural changes.** Batching + parallel-safety; the immediate
|
|
||||||
path is already iteration-safe, so lower urgency.
|
|
||||||
- **LC6 — supervision / restart / migration.** Waits on a failure model and the
|
|
||||||
hot-reload roadmap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Open decisions
|
|
||||||
|
|
||||||
1. **Reason enum (LC1):** *resolved for `@OnDespawn`* — ships `Despawned`,
|
|
||||||
`SceneExit`, `Quit` as a compiler-owned `EndReason`, passed as an optional
|
|
||||||
`reason:` binding (not a separate annotation). Still open: `SceneExit` has no
|
|
||||||
firing site until scene-owned entities (SCENES-DESIGN E1); should `@OnDetach`
|
|
||||||
and scene `on exit` take reasons too, and if so with which reason values?
|
|
||||||
2. **`@OnChange` (LC2):** fire on every write (simple, over-fires) or only on an
|
|
||||||
actual value change (needs the old value at the store site)? Per-field or
|
|
||||||
whole-property granularity?
|
|
||||||
3. **Membership edges (LC3):** where do the shadow bits live, and is the check
|
|
||||||
per-frame or event-driven off attach/detach/spawn? Cost budget.
|
|
||||||
4. **Keyed effects (LC4):** syntax (`@Effect` annotation vs an `effect { … dispose
|
|
||||||
{ … } }` statement), and where per-entity teardown/key state is stored.
|
|
||||||
5. **Ordering:** none of this addresses intra-phase handler ordering (Bevy
|
|
||||||
`before`/`after`, flecs `DependsOn`). Worth a separate proposal; declarative
|
|
||||||
relational ordering over priority integers, per the survey.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Companion to [LANGUAGE.md §Annotations](LANGUAGE.md) and
|
|
||||||
[SCENES-DESIGN.md](SCENES-DESIGN.md) (scene-owned entities and reasons intersect at
|
|
||||||
LC1/LC5). Supersedes nothing until the compiler work in §12 lands.*
|
|
||||||
1560
LUANTI-ROADMAP.md
1560
LUANTI-ROADMAP.md
File diff suppressed because it is too large
Load diff
338
MOBILE-DESIGN.md
338
MOBILE-DESIGN.md
|
|
@ -1,338 +0,0 @@
|
||||||
# iOS & Android — a design doc
|
|
||||||
|
|
||||||
> **Status: all design, nothing shipped.** Ludic builds windowed on macOS
|
|
||||||
> (`runtime/native/cocoa.ll`) and has a documented — but currently un-reimplemented
|
|
||||||
> — wasm32 web target. iOS and Android are not buildable today, and the
|
|
||||||
> cross-compile plumbing that would target them died with the C driver. This doc
|
|
||||||
> lays out the whole path so we can decide the shape before building any of it. The
|
|
||||||
> headline decision (§7): render on the **GPU via `extern fn` FFI**, not the CPU
|
|
||||||
> framebuffer. §11 lists the open decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Where we are
|
|
||||||
|
|
||||||
A Ludic program compiles to LLVM IR, then clang assembles and links it. The
|
|
||||||
platform story has **two independent axes**, and it's essential not to conflate
|
|
||||||
them:
|
|
||||||
|
|
||||||
| Axis | What it is | State today |
|
|
||||||
|---|---|---|
|
|
||||||
| **Target** (triple + toolchain) | how IR becomes a runnable binary for an OS/arch | barely plumbed — no `--target`, no emitted `target triple`, host-only |
|
|
||||||
| **Platform runtime** (window/input/present) | one file implementing the 5-function window protocol | well-factored — `cocoa.ll` is ~328 lines, swappable |
|
|
||||||
|
|
||||||
**What exists:**
|
|
||||||
|
|
||||||
- The window seam is exactly five functions — `win_open` / `win_poll` /
|
|
||||||
`win_present` / `win_running` / `win_close` — declared by the compiler
|
|
||||||
([emit_head.ludic:58](selfhost/emit_head.ludic:58)) and lowered as intrinsics
|
|
||||||
([emit_intrin2.ludic:39](selfhost/emit_intrin2.ludic:39)). The runtime calls them
|
|
||||||
through `rt_*` wrappers ([core.ludic:48](runtime/native/core.ludic:48),
|
|
||||||
[:103](runtime/native/core.ludic:103), [:217](runtime/native/core.ludic:217)).
|
|
||||||
`COMPILING.md` states the intent plainly: a new platform is "another `.ll` file
|
|
||||||
with the same five entry points and no compiler change."
|
|
||||||
- **`extern fn` FFI is real and live** — `extern function c_hypot(a: fixed, b: fixed) ->
|
|
||||||
fixed = "hypot_fx"` ([LANGUAGE.md:565](LANGUAGE.md:565)), with a full pipeline:
|
|
||||||
parse ([parse_game.ludic:236](selfhost/parse_game.ludic:236)) → call lowering to a
|
|
||||||
direct `call @<sym>` ([emit_expr.ludic:168](selfhost/emit_expr.ludic:168)) →
|
|
||||||
`declare` emission ([emit_head.ludic:105](selfhost/emit_head.ludic:105)). Working
|
|
||||||
examples: [examples/networking/net_echo.ludic:12](examples/networking/net_echo.ludic:12),
|
|
||||||
[examples/library/arena.ludic:14](examples/library/arena.ludic:14). This is the single
|
|
||||||
most important fact in this document — see §7.
|
|
||||||
|
|
||||||
**What's missing (all of it must be built):**
|
|
||||||
|
|
||||||
| Gap | Why mobile needs it |
|
|
||||||
|---|---|
|
|
||||||
| `--target <triple>` flag + emitted `target triple`/`datalayout` | iOS = `aarch64-apple-ios`, Android = `aarch64-linux-android`; both are cross-compiles |
|
|
||||||
| per-target `size_t` width (i32/i64) | already a known wasm trap; every allocation sizing depends on it |
|
|
||||||
| **OS-owned frame loop** (`ludic_boot`/`ludic_frame`/`ludic_alive`/`ludic_teardown`) | iOS (CADisplayLink) and Android (Choreographer) own the loop — you cannot `while(alive)` |
|
|
||||||
| per-platform window shim + touch input | UIKit/`CAMetalLayer`, Android `Surface`/NDK; input is touch, not a keycode |
|
|
||||||
| SDK sysroot + packaging + signing | `.app` bundle / `.apk`, not a bare executable |
|
|
||||||
|
|
||||||
The frame-loop gap is shared with the web target — `tools/ludic-web/run.mjs`
|
|
||||||
already expects `ludic_boot`/`ludic_frame`, but the self-hosted emitter only
|
|
||||||
produces a monolithic `@main` ([emit_game.ludic:685](selfhost/emit_game.ludic:685)).
|
|
||||||
So the wasm path is half-broken for the same reason mobile can't exist yet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Design principles
|
|
||||||
|
|
||||||
1. **Two axes, kept separate.** "Add a platform" = a cross-compile *target* plus a
|
|
||||||
platform *runtime*. Muddling them is why this looks bigger than it is. Most of
|
|
||||||
the compiler work (§4, §5) is target plumbing that serves web, iOS, and Android
|
|
||||||
at once; the per-OS work (§6) is genuinely small by design.
|
|
||||||
2. **The OS owns the loop — so we must too.** Mobile, like the browser, forbids an
|
|
||||||
inline frame loop. Rather than special-case mobile, adopt the frame-driven model
|
|
||||||
*everywhere* the OS demands it, from one emitter change. This is the keystone.
|
|
||||||
3. **The GPU is an ABI to call, not a program to compile.** `extern fn` already
|
|
||||||
binds C libraries; bind GL ES / Metal the same way. No IR-per-API (the `cocoa.ll`
|
|
||||||
route — 328 lines for *five* functions), no per-symbol intrinsics. The roadmap
|
|
||||||
reaches this conclusion independently ([LUANTI-ROADMAP.md:1083](LUANTI-ROADMAP.md:1083),
|
|
||||||
[:1375](LUANTI-ROADMAP.md:1375)).
|
|
||||||
4. **The 2D stack stays byte-identical.** The framebuffer graphics
|
|
||||||
(`rt_fb` + all `rt_*`/`image`/`truetype`/`ui` primitives) keep working
|
|
||||||
unchanged. GPU rendering is *additive*: 2D composites as one texture on top of
|
|
||||||
GPU 3D. Nothing above the window seam is rewritten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Core model
|
|
||||||
|
|
||||||
Everything below reduces to plumbing one new flag through the compiler and swapping
|
|
||||||
two runtime files per OS. The mental model:
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc app.ludic --target aarch64-apple-ios -o app
|
|
||||||
│
|
|
||||||
├─ emit_head: target triple / datalayout / size_t width (§4)
|
|
||||||
├─ emit_game: ludic_boot/frame/alive/teardown not @main (§5)
|
|
||||||
├─ link: runtime/ios/uikit.ll + gfx3d.ldylib (§6, §7)
|
|
||||||
└─ package: .app bundle + codesign (§8)
|
|
||||||
```
|
|
||||||
|
|
||||||
The game source and the entire ECS/graphics/UI stack compile **unchanged** for
|
|
||||||
every target. Only the head declarations, the entry-point shape, the linked
|
|
||||||
platform file, and the packaging step vary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Extension M1 — the target axis: `--target`, triple, `size_t`
|
|
||||||
|
|
||||||
Today [main.ludic:66](selfhost/main.ludic:66) parses `--windowed`/`--headless`/
|
|
||||||
`--emit-llvm`/… and nothing selects an arch; the IR carries no `target triple`, so
|
|
||||||
native inherits clang's host default and the only explicit triple in the tree is
|
|
||||||
`wasm32-unknown-unknown` ([runtime/web/wasm.ll:23](runtime/web/wasm.ll:23)).
|
|
||||||
|
|
||||||
Proposal: a `--target <triple>` flag that drives three things.
|
|
||||||
|
|
||||||
```
|
|
||||||
ludicc app.ludic --target aarch64-apple-ios -o app
|
|
||||||
ludicc app.ludic --target aarch64-apple-ios-simulator -o app # x86_64 host → arm64 sim varies
|
|
||||||
ludicc app.ludic --target aarch64-linux-android -o libapp.so
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Emit the triple + datalayout.** `emit_header`
|
|
||||||
([emit_head.ludic:37](selfhost/emit_head.ludic:37)) gains a `target triple = …`
|
|
||||||
/ `target datalayout = …` line, chosen from a small table keyed on `--target`.
|
|
||||||
Absent the flag, emit nothing (host default) — keeps existing native builds
|
|
||||||
byte-identical.
|
|
||||||
- **Per-target `size_t` width.** wasm32 already needs `i32` sizes; the same helper
|
|
||||||
discipline (`ll_size_t`/`ll_widen`/`ll_narrow`, per the web-backend notes) applies
|
|
||||||
to any 32-bit target. iOS/Android arm64 are LP64 like macOS, so `i64` — but the
|
|
||||||
flag must *select* the width, not assume the host's.
|
|
||||||
- **Toolchain construction.** The linker command
|
|
||||||
([main.ludic:130](selfhost/main.ludic:130)) becomes target-conditional: an SDK
|
|
||||||
sysroot (`-isysroot`/`--sysroot`), the platform `.ll`, and target-specific link
|
|
||||||
flags (§8). `$LUDIC_CC` still overrides; add `$LUDIC_SYSROOT_<target>` for the
|
|
||||||
SDK path so CI and local machines can differ.
|
|
||||||
|
|
||||||
This axis is **shared with reviving wasm** — do it once, three targets benefit.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Extension M2 — the OS-owned frame loop (the keystone)
|
|
||||||
|
|
||||||
A native build emits `@main` with the frame loop inline — an `rt_init`, then a
|
|
||||||
`loop:`/`done:` block calling `rt_poll`/`rt_running`
|
|
||||||
([emit_game.ludic:685](selfhost/emit_game.ludic:685)). **iOS and Android cannot run
|
|
||||||
this.** UIKit calls back into your code once per display refresh (CADisplayLink);
|
|
||||||
Android's Choreographer does the same; the browser's `requestAnimationFrame` already
|
|
||||||
does. In all three the OS owns the loop and calls *you*.
|
|
||||||
|
|
||||||
Proposal: emit four exported functions instead of an inline-loop `@main`, exactly
|
|
||||||
as `COMPILING.md` already describes and `run.mjs` already expects:
|
|
||||||
|
|
||||||
```
|
|
||||||
ludic_boot() → rt_init (once)
|
|
||||||
ludic_frame() → rt_poll · systems · rt_present (per OS callback)
|
|
||||||
ludic_alive() → i1 → rt_running (OS asks: keep going?)
|
|
||||||
ludic_teardown() → rt_shutdown (once)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`@main` becomes the composed default, not the only shape.** For host desktop
|
|
||||||
and headless, the compiler synthesizes an `@main` that *calls* the four in an
|
|
||||||
inline loop — so native/headless output is unchanged in behavior. For
|
|
||||||
OS-owned-loop targets (`--target` is wasm/ios/android, or a new
|
|
||||||
`--loop=external` mode), emit only the four exports and no driving `@main`.
|
|
||||||
- **One emitter change, three targets fixed.** This simultaneously un-breaks the
|
|
||||||
web target (whose runner already calls these) and unlocks both mobile OSes. It is
|
|
||||||
the highest-leverage change in this doc.
|
|
||||||
- **State stays where it is.** The four functions close over the same globals
|
|
||||||
`rt_init`/`rt_poll`/`rt_running`/`rt_shutdown` already touch
|
|
||||||
([core.ludic:48](runtime/native/core.ludic:48)); no new runtime state, no heap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Extension M3 — the per-OS window shim + touch input
|
|
||||||
|
|
||||||
Each OS gets one platform file implementing the five-function seam, modeled on
|
|
||||||
`cocoa.ll` but rewritten for its UI toolkit. This is the part the codebase is
|
|
||||||
explicitly built for.
|
|
||||||
|
|
||||||
- **iOS — `runtime/ios/uikit.ll` (or a thin `.m` shim).** `win_open` creates a
|
|
||||||
`UIWindow` + a `UIViewController` whose view is a `CAMetalLayer`/`MTKView`;
|
|
||||||
`win_present` presents the current drawable; the loop is driven by M2's
|
|
||||||
`ludic_frame` from a `CADisplayLink`, so `win_poll`/`win_running` adapt to the
|
|
||||||
callback model rather than a spin. Hand-written IR against `objc_msgSend` is
|
|
||||||
possible (it's how `cocoa.ll` works) but a small compiled `.m` linked in is more
|
|
||||||
maintainable for UIKit's larger surface — an open decision (§11).
|
|
||||||
- **Android — `runtime/android/ndk.ll` + a Kotlin/Java `Activity` host.** The
|
|
||||||
native code is a `.so` loaded by an `Activity`; the window is an
|
|
||||||
`ANativeWindow`/`Surface` obtained via `GameActivity`/NDK, GPU via EGL + GL ES.
|
|
||||||
Frames are driven by Choreographer through JNI into `ludic_frame`.
|
|
||||||
- **Touch input changes the input seam.** `win_poll()` returns a single `int`
|
|
||||||
keycode today ([emit_intrin2.ludic:41](selfhost/emit_intrin2.ludic:41),
|
|
||||||
[core.ludic:217](runtime/native/core.ludic:217)) — insufficient for touch, which
|
|
||||||
needs `(x, y, phase, id)`. Options: (a) a parallel `win_poll_touch() -> pointer`
|
|
||||||
draining an event queue, or (b) widen the input model to a small event struct for
|
|
||||||
all platforms. This is the one place mobile forces a decision above the window
|
|
||||||
seam. Proposed: add touch as a **separate** seam so keyboard platforms stay
|
|
||||||
untouched and byte-identical.
|
|
||||||
|
|
||||||
Everything above the seam — framebuffer, PNG sprites, TrueType, retained UI — is
|
|
||||||
portable Ludic and compiles unchanged.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Extension M4 — GPU rendering via `extern fn` (the headline)
|
|
||||||
|
|
||||||
Today **all** drawing writes into one CPU framebuffer: `rt_fb`, a
|
|
||||||
`words(320*240)` buffer of `0x00RRGGBB` i32 pixels
|
|
||||||
([core.ludic:23](runtime/native/core.ludic:23)), written by every primitive
|
|
||||||
(`rt_clear`/`rt_fill_rect`/glyphs/`rt_blend_px`/`tt_blit`/UI) and handed whole to
|
|
||||||
`win_present`. `cocoa.ll` blits it through CoreGraphics —
|
|
||||||
`CGBitmapContextCreate`→`CGImage`→`CGContextDrawImage` inside `@ludic_drawRect`
|
|
||||||
([cocoa.ll:94](runtime/native/cocoa.ll:94)). There is no GPU context anywhere.
|
|
||||||
|
|
||||||
Because **`extern fn` already exists**, binding the GPU is ordinary runtime code —
|
|
||||||
no new language feature, no new intrinsic:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — runtime/native/gfx3d.ludic, illustrative
|
|
||||||
extern function gl_gen_textures(n: int, out: pointer) -> void = "glGenTextures"
|
|
||||||
extern function gl_tex_image_2d(t: int, w: int, h: int, px: pointer) -> void = "gl_tex_image_2d"
|
|
||||||
extern function gl_draw_elements(mode: int, count: int, ty: int, idx: pointer) -> void = "glDrawElements"
|
|
||||||
```
|
|
||||||
|
|
||||||
Two phases, additive:
|
|
||||||
|
|
||||||
1. **Framebuffer-as-texture (drop-in).** Keep the entire 2D stack. `rt_present`
|
|
||||||
([core.ludic:103](runtime/native/core.ludic:103)) uploads `rt_fb` as one texture
|
|
||||||
and draws a full-screen quad. The `win_present(fb,w,h)` signature is unchanged;
|
|
||||||
only the pixel-delivery core of the platform file differs (texture upload instead
|
|
||||||
of CoreGraphics blit). This is the minimum viable GPU path and gets mobile on
|
|
||||||
screen with zero changes above the seam.
|
|
||||||
2. **True GPU 3D (additive).** Geometry goes straight to GL/Metal via `gfx3d.ludic`
|
|
||||||
`extern fn` calls; the CPU framebuffer is reused only for the 2D UI overlay,
|
|
||||||
composited as a texture on top. New GPU-draw entry points live in `gfx3d.ludic`
|
|
||||||
as `extern fn`s — the five-function window protocol does **not** widen.
|
|
||||||
|
|
||||||
Language-level cost is narrow and already scoped by the roadmap:
|
|
||||||
|
|
||||||
- **`f32`** (roadmap gate G-04) for vertex/matrix data — the *only* hard language
|
|
||||||
dependency ([LUANTI-ROADMAP.md:1087](LUANTI-ROADMAP.md:1087)).
|
|
||||||
- Optional vector operator overloading for `v3f`/`m4` ergonomics (G-29,
|
|
||||||
[:1107](LUANTI-ROADMAP.md:1107)) — a "nicer, not necessary."
|
|
||||||
|
|
||||||
The roadmap's own decision is explicit: FFI over IR-per-API, because "`cocoa.ll`
|
|
||||||
is 327 lines for *five* window functions — OpenGL has hundreds of entry points"
|
|
||||||
([LUANTI-ROADMAP.md:1375](LUANTI-ROADMAP.md:1375)).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Extension M5 — packaging, SDKs, and signing
|
|
||||||
|
|
||||||
The current driver is one `clang` call ([main.ludic:130](selfhost/main.ludic:130))
|
|
||||||
producing a bare binary. Mobile output is a bundle, and this is where most
|
|
||||||
real-world friction lives — it is deliberately the *last* phase.
|
|
||||||
|
|
||||||
- **iOS.** Cross-compile with the iPhoneOS SDK sysroot → an executable, wrap in an
|
|
||||||
`App.app` bundle with an `Info.plist`, `codesign` with a development identity,
|
|
||||||
install to simulator/device. Simulator is the cheap inner loop
|
|
||||||
(`aarch64-apple-ios-simulator`); device needs a provisioning profile. ludicc
|
|
||||||
should emit the binary and shell a packaging step (or emit a manifest a small
|
|
||||||
script consumes), not learn Xcode's project format.
|
|
||||||
- **Android.** Cross-compile with the NDK → `libapp.so`, drop it into a minimal
|
|
||||||
Gradle/Kotlin `Activity` shell, build the `.apk`/`.aab`, sign with a keystore.
|
|
||||||
The `Activity` is fixed boilerplate that ships in the repo (`runtime/android/`),
|
|
||||||
parameterized by app name/id.
|
|
||||||
- **Keep the compiler out of it.** Both flows are "produce native code + assemble a
|
|
||||||
package around it." The compiler's job ends at the object/`.so`; a `--package`
|
|
||||||
step or an external `build-mobile.sh` owns the bundle. This mirrors how ludicc
|
|
||||||
already drives clang without becoming a build system.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Lowering / build summary
|
|
||||||
|
|
||||||
| Construct | Reduces to |
|
|
||||||
|---|---|
|
|
||||||
| `--target <triple>` (M1) | a triple/datalayout line in `emit_header` + a `size_t`-width choice + target-conditional link command |
|
|
||||||
| OS-owned loop (M2) | emit `ludic_boot`/`ludic_frame`/`ludic_alive`/`ludic_teardown`; host/headless get a synthesized `@main` calling them |
|
|
||||||
| window shim (M3) | one `.ll`/shim per OS implementing the same five `win_*` intrinsics; no compiler change |
|
|
||||||
| touch input (M3) | a **new, separate** input seam (`win_poll_touch`), so keycode platforms stay byte-identical |
|
|
||||||
| framebuffer→texture (M4.1) | `rt_present` uploads `rt_fb` as a texture + full-screen quad; `win_present` signature unchanged |
|
|
||||||
| GPU 3D (M4.2) | `extern fn` calls in `runtime/native/gfx3d.ludic` — data in `prog`, zero compiler edits, needs only `f32` |
|
|
||||||
| packaging (M5) | binary/`.so` unchanged; an external `--package`/script builds `.app`/`.apk` and signs |
|
|
||||||
|
|
||||||
No new allocator, no new dispatch, no per-API intrinsics. The game and the 2D
|
|
||||||
graphics stack compile identically for every target; only head declarations, the
|
|
||||||
entry-point shape, the linked platform file, and packaging vary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Suggested implementation phases
|
|
||||||
|
|
||||||
Each is independently shippable and testable, matching how the repo phases work.
|
|
||||||
|
|
||||||
- **M0 — target axis** (M1) + **revive the OS-owned loop** (M2). *Do these first
|
|
||||||
and together* — they're the shared compiler plumbing, they un-break the existing
|
|
||||||
web target (proving the frame-loop split against `run.mjs`/`bin/x test` before any
|
|
||||||
mobile SDK is involved), and they need no mobile toolchain. This is the floor.
|
|
||||||
- **M1 — iOS simulator, framebuffer-as-texture** (M3 iOS shim + M4.1). First pixels
|
|
||||||
on a phone, GL/Metal binding proven, no signing/device friction yet.
|
|
||||||
- **M2 — iOS device** (M5 iOS packaging + signing).
|
|
||||||
- **M3 — Android** (M3 Android shim + M4.1 + M5 Android packaging), reusing every
|
|
||||||
M0 change.
|
|
||||||
- **M4 — `f32` + GPU 3D** (M4.2), gated on roadmap G-04; the additive 3D path over
|
|
||||||
`gfx3d.ludic`.
|
|
||||||
- **M5 (later) — touch-input model** hardening (M3), gesture/multitouch, once a real
|
|
||||||
app exercises it.
|
|
||||||
|
|
||||||
M0 is the honest prerequisite and the highest-leverage work — it serves three
|
|
||||||
targets and revives a fourth. M1 is the first thing anyone can *see*.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Open decisions
|
|
||||||
|
|
||||||
1. **Loop selection:** does `--target ios/android/wasm` *imply* the external loop,
|
|
||||||
or is there an explicit `--loop=external` flag? (Proposed: implied by target,
|
|
||||||
with the flag as an override for headless testing.)
|
|
||||||
2. **iOS shim language:** hand-written `.ll` against `objc_msgSend` like `cocoa.ll`,
|
|
||||||
or a small compiled `.m`? (Proposed: `.m` — UIKit's surface is too large for
|
|
||||||
maintainable IR, and Metal setup is verbose.)
|
|
||||||
3. **Touch seam shape:** a separate `win_poll_touch` queue, or a unified event
|
|
||||||
struct replacing the keycode `win_poll` on all platforms? (Proposed: separate,
|
|
||||||
to keep desktop/web byte-identical.)
|
|
||||||
4. **GPU API baseline:** GL ES 3.0 everywhere (Android native, iOS via ANGLE/Metal
|
|
||||||
translation), or Metal on iOS + GL ES on Android from day one? (Proposed: GL ES
|
|
||||||
3.0 first for a single codepath; Metal later.)
|
|
||||||
5. **Android host:** ship a fixed Kotlin `GameActivity` in `runtime/android/`, or
|
|
||||||
generate it per app? (Proposed: fixed boilerplate, parameterized by name/id.)
|
|
||||||
6. **Packaging home:** a `--package` step inside ludicc, or an external
|
|
||||||
`build-mobile.sh`? (Proposed: external script; keep the compiler out of bundle
|
|
||||||
formats.)
|
|
||||||
7. **`size_t` for arm64:** confirm iOS/Android arm64 are LP64 (`i64`) in the width
|
|
||||||
table, and that the `ll_size_t` discipline covers every new size-taking call.
|
|
||||||
8. **Simulator arch:** how to handle `aarch64-apple-ios-simulator` vs. x86_64 sim on
|
|
||||||
Intel hosts in the target table.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Companion to [COMPILING.md](COMPILING.md) (§ toolchain, the wasm frame-loop
|
|
||||||
split), [LANGUAGE.md §"Functions & FFI"](LANGUAGE.md:560) (`extern fn`), and
|
|
||||||
[LUANTI-ROADMAP.md](LUANTI-ROADMAP.md) (G-04 `f32`, G-28 GPU FFI, G-29 3D math).
|
|
||||||
Supersedes nothing until the M0 compiler work lands.*
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
# Networking, from primitives up — a design doc
|
|
||||||
|
|
||||||
> **Status: N0–N6 all shipped.** The whole stack is implemented and self-hosted,
|
|
||||||
> and — unlike the original N0/N1 which linked C hosts — every phase now runs as a
|
|
||||||
> self-contained **pure-Ludic** program (no `.c`, no foreign host): a built-in
|
|
||||||
> loopback transport fills the seam, and each `examples/net_*.ludic` drives and
|
|
||||||
> asserts itself from its own `entry`. See `bin/x test` (checks `net_echo` … `net_demo`)
|
|
||||||
> and `examples/networking/net_demo.ludic` for a full RPC→authority→replicate→reconcile loop.
|
|
||||||
> clang remains only as the LLVM-IR assembler/linker (no C is compiled), the floor
|
|
||||||
> Rust and Swift stand on.
|
|
||||||
>
|
|
||||||
> _Historical note:_ **N0 + N1 shipped first; N2–N6 were design.** Two phases landed as `bin/x test`
|
|
||||||
> checks. **N0 (transport seam):** `extern fn` now lowers end to end — a direct
|
|
||||||
> `@<sym>` call plus a `declare`, no networking logic in the compiler — so the whole
|
|
||||||
> transport is two externs (`net_send`/`net_poll`) a host fills. Proven by
|
|
||||||
> [`examples/networking/net_echo.ludic`](examples/networking/net_echo.ludic) sending four bytes through
|
|
||||||
> the loopback host in [`tests/net_c/loopback.c`](tests/net_c/loopback.c) and
|
|
||||||
> polling them back (`4 10 20 30 42`). **N1 (snapshot-to-buffer):**
|
|
||||||
> `world_size()`/`world_save(buf)`/`world_load(buf, len)` generalize `save()`/`load()`
|
|
||||||
> from a file to a caller-owned memory buffer — the same block layout via `memcpy` —
|
|
||||||
> so the whole ECS world round-trips through bytes. Proven by
|
|
||||||
> [`examples/networking/net_snapshot.ludic`](examples/networking/net_snapshot.ludic) +
|
|
||||||
> [`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) (snapshot, mutate,
|
|
||||||
> restore → `50 7 50`). Both are byte-identical when unused, so the offline dividend
|
|
||||||
> (§8) holds. This is a companion to
|
|
||||||
> [EVENTS-DESIGN.md](EVENTS-DESIGN.md), [LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md),
|
|
||||||
> and [SCENES-DESIGN.md](SCENES-DESIGN.md). Where the events work made Ludic
|
|
||||||
> *moddable*, this proposes making it *networked* — and it deliberately does **not**
|
|
||||||
> ship a multiplayer framework. Ludic is a language: it exposes the low-level
|
|
||||||
> mechanism (transport seam, world snapshot, generated serializers, ownership, a
|
|
||||||
> drivable sim) and a thin high-level *declarative* layer that lowers onto that
|
|
||||||
> mechanism, and it leaves the netcode *policy* (authority, prediction, relevancy)
|
|
||||||
> to the developer or a library. §14 lists the open decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Thesis
|
|
||||||
|
|
||||||
Every networking model dies on one of two problems: **determinism** or **state
|
|
||||||
serialization**. Ludic already solves both, almost by accident.
|
|
||||||
|
|
||||||
- **Determinism** is designed in — seeded RNG, `fixed` (Q16.16) instead of floats,
|
|
||||||
byte-identical golden renders, and (as of [EVENTS-DESIGN EV6](EVENTS-DESIGN.md))
|
|
||||||
bounded, array-ordered event dispatch. A modded, event-driven Ludic game still
|
|
||||||
replays identically. That is exactly the property lockstep multiplayer needs, and
|
|
||||||
the reason Factorio's heavily-modded multiplayer stays in sync.
|
|
||||||
- **State serialization** already exists — `save()`/`load()` snapshot the *entire*
|
|
||||||
ECS World to a byte buffer ([`selfhost/emit_save.ludic`](selfhost/emit_save.ludic)),
|
|
||||||
and the world-table schema built for [EVENTS-DESIGN EV2](EVENTS-DESIGN.md) (prop →
|
|
||||||
field → offset) is exactly the descriptor you serialize against.
|
|
||||||
|
|
||||||
So networking is not a new subsystem. It is a **fourth lens on the event + world
|
|
||||||
layer** — the same layer modding used. And it obeys the same two-altitude rule as
|
|
||||||
everything else in Ludic:
|
|
||||||
|
|
||||||
> **Low-level is freedom; high-level is developer experience; they are the same
|
|
||||||
> feature at two altitudes.** `@Queries` lowers to a query loop, `scene` lowers to a
|
|
||||||
> machine, `@Public @OnSpawn` lowers to `emit`. Networking's high-level annotations
|
|
||||||
> lower to a transport seam, generated serializers, and a drivable sim — and the
|
|
||||||
> primitives stay exposed underneath for anyone the sugar doesn't fit.
|
|
||||||
|
|
||||||
The developer writes **one simulation**, declares *what* replicates, *who* owns
|
|
||||||
each entity, and *where* each handler runs — and never branches on `is_server()`
|
|
||||||
in ordinary code. The compiler lowers the declarations; a networking *runtime*
|
|
||||||
(the seam-filler, like `rt_*` for windowing) supplies the transport and the tick.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Two altitudes, one system
|
|
||||||
|
|
||||||
| Altitude | Who writes it | Surface |
|
|
||||||
|---|---|---|
|
|
||||||
| **High-level (DX)** | the developer, declaratively | `@Sync` (field/property/model), `@Owned`, `@Server`/`@Predicted`, directional remote events |
|
|
||||||
| **Lowering** | the compiler | per-model serializers, role-guarded dispatch, remote-event send/recv, ownership storage |
|
|
||||||
| **Runtime seam** | a networking library (blessed or custom) | binds the socket, sets `role`, drives the replication tick |
|
|
||||||
| **Low-level (freedom)** | power users, when the sugar doesn't fit | `net_send`/`net_poll`, `world_save`/`world_load`, generated `serialize_*`/`apply_*`, `owner()`, the drivable sim |
|
|
||||||
|
|
||||||
Everyone lives at the top row for normal games; the bottom row stays open for
|
|
||||||
someone building something no framework could express. The split that keeps this a
|
|
||||||
*language* and not a *framework*: **annotations and their lowering are the language;
|
|
||||||
the replication driver and the transport are a library.** It is precisely the
|
|
||||||
events story — `@On`/`emit` are the language, the *modding system* is library code —
|
|
||||||
applied again.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Research digest — the one idea to steal from each
|
|
||||||
|
|
||||||
| System | The transferable idea |
|
|
||||||
|---|---|
|
|
||||||
| **Quake / QuakeWorld** | The founding pattern: **client-side prediction + server reconciliation**, and delta-compressed snapshots against the last acked baseline. Predict locally, correct from the authority. |
|
|
||||||
| **Source (Valve)** | **Entity interpolation** (render remote entities slightly in the past, smoothly) paired with **lag compensation** (the server rewinds to the shooter's view for hit detection). Interpolation and rewind are two halves of one clock discipline. |
|
|
||||||
| **Unity NGO** (GameObject) | `NetworkVariable<T>` with **read/write permissions** + `OnValueChanged`; ownership as `OwnerClientId`. Also the **anti-pattern to avoid**: `IsServer`/`IsOwner` branching sprinkled through gameplay code. |
|
|
||||||
| **Unity Netcode for Entities** (ghosts) | The model Ludic is closest to: **replication is a compile-time property of components and fields** — `[GhostField]`, `[GhostComponent]`, `GhostOwner`, and `Predicted`/`Interpolated` ghost modes — with serializers *generated* from the ECS schema. |
|
|
||||||
| **Mirror / FishNet** | The community-ergonomic take: `SyncVar` with change **hooks**, and clean **directional RPCs** — `Command` (client→server) / `ClientRpc` (server→clients). |
|
|
||||||
| **GGPO / rollback** | Save state → predict → on misprediction **restore and re-simulate**. Its one hard requirement is *cheap, complete state snapshot/restore* — which Ludic already has in `save()`/`load()`. |
|
|
||||||
| **Factorio** | Fully **deterministic lockstep** for heavy mod multiplayer: only *inputs* cross the wire; the whole sim is reproduced. Proof that determinism (EV6) is the enabler, not a nicety. |
|
|
||||||
| **Photon Quantum** | A shipping product that *is* deterministic-ECS-rollback. Validates the exact combination — ECS + determinism + rollback — Ludic is already positioned for. |
|
|
||||||
| **Roblox** | The **local/remote split** (`BindableEvent` vs `RemoteEvent`), server-authority by default, and engine-replicated properties: "some state just replicates, and RPCs are directional events." |
|
|
||||||
|
|
||||||
Six **footguns** the survey warns against, to design *out* from the start:
|
|
||||||
|
|
||||||
1. **Role branching everywhere.** `if (IsServer)` scattered through gameplay is the
|
|
||||||
NGO readability tax. Fix: **role is a handler annotation** (`@Server`/`@Predicted`),
|
|
||||||
never a runtime branch in ordinary code.
|
|
||||||
2. **Float nondeterminism.** Lockstep breaks the instant the networked sim touches
|
|
||||||
`f32` across platforms. Fix: the determinism contract (§11) — the networked sim
|
|
||||||
stays `int`/`fixed`.
|
|
||||||
3. **Replicating pointers / heap refs.** A `ptr` field holds a machine-local
|
|
||||||
address; it cannot cross the wire. Fix: **the compiler rejects `@Sync` on a
|
|
||||||
non-POD-scalar field** — a checked guarantee, not a convention.
|
|
||||||
4. **Sending everything every tick.** Fix: `@Sync` is **opt-in at the field level**
|
|
||||||
(only marked fields replicate), plus change-driven dirty tracking (`@OnChange`,
|
|
||||||
[LIFECYCLE LC2](LIFECYCLE-DESIGN.md)) so an unchanged field costs nothing.
|
|
||||||
5. **Hidden authority.** Magic "the server decides" behavior is unclear and
|
|
||||||
unauditable. Fix: **explicit** `@Server`/`@Predicted`; unmarked code runs
|
|
||||||
everywhere by definition.
|
|
||||||
6. **Schema-less snapshots.** A raw state blob with no version desyncs silently on a
|
|
||||||
version mismatch. Fix: the **world-table schema is the versioned descriptor** the
|
|
||||||
serializer is generated against.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. What Ludic already has
|
|
||||||
|
|
||||||
The substrate is unusually complete for an engine that has never networked:
|
|
||||||
|
|
||||||
- **A deterministic simulation** — seeded RNG, `fixed` math, ordered ECS iteration,
|
|
||||||
EV6-bounded event dispatch. Lockstep's precondition.
|
|
||||||
- **World snapshot/restore** — `save()`/`load()` serialize the whole World
|
|
||||||
([emit_save.ludic](selfhost/emit_save.ludic)); today to a file, trivially
|
|
||||||
retargetable to a memory buffer. Rollback's precondition.
|
|
||||||
- **A reflective world table** — `ludic_get`/`set`/`has`/`query`/`register_prop`
|
|
||||||
and the prop→field→offset schema (EV2/EV2b). The apply-and-serialize substrate.
|
|
||||||
- **An event bus with a foreign ABI and POD payloads** (EV0). Directional remote
|
|
||||||
events (RPCs) are one flag on this.
|
|
||||||
- **The `rt_*` seam pattern** — the compiler already emits calls to
|
|
||||||
`rt_init`/`rt_poll`/`rt_present` that a runtime library fills. Networking's
|
|
||||||
transport and role registers plug into the identical seam.
|
|
||||||
|
|
||||||
What is missing is small and named: a transport seam, snapshot-to-*buffer*,
|
|
||||||
generated per-field serializers, ownership storage, role-guarded dispatch, and a
|
|
||||||
developer-drivable loop. Each is a phase in §13.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. The low-level primitives (the freedom layer)
|
|
||||||
|
|
||||||
Unopinionated, composable, host- or developer-owned. A power user builds any model
|
|
||||||
directly from these; the high-level layer (§6) is sugar over them.
|
|
||||||
|
|
||||||
| Primitive | Signature (sketch) | Enables |
|
|
||||||
|---|---|---|
|
|
||||||
| **Transport seam** | `extern function net_send(peer: int, buf: pointer, len: int)` · `extern function net_poll(buf: pointer, cap: int) -> int` | any model; host binds UDP (native) or WebRTC/WebSocket (wasm), or a loopback for tests |
|
|
||||||
| **World snapshot ↔ buffer** | `world_save(buf: pointer) -> int` · `world_load(buf: pointer, len: int)` | rollback, replication, join/resync — generalizes `save()`/`load()` off the filesystem |
|
|
||||||
| **Generated serializers** | `serialize_<Model>(e: entity, buf: pointer) -> int` · `apply_<Model>(e: entity, buf: pointer, len: int)` | per-model, touch only the `@Sync` fields; emitted from the schema |
|
|
||||||
| **Ownership** | `owner(e: entity) -> int` · `set_owner(e: entity, id: int)` | authority checks, per-entity owner metadata (an `@L_owner` array, like `@L_kind`) |
|
|
||||||
| **Role registers** | `is_server() -> bool` · `is_owner(e: entity) -> bool` · `local_id() -> int` | the runtime sets these; role-guarded dispatch reads them |
|
|
||||||
| **Drivable sim** | `tick_fixed()` · `tick_render()` · seed get/set | a developer-owned loop for prediction/rollback (also: replay, headless tests, AI) |
|
|
||||||
| **Remote-event serde** | `emit`-site serialize + `net_send`; inbound bytes rebuild + re-`emit` | RPCs |
|
|
||||||
|
|
||||||
Transport is the one that needs *no* language work at all — a developer can already
|
|
||||||
`extern fn` a socket library and link it, exactly as the windowing layer is linked.
|
|
||||||
The language's genuine contributions are snapshot-to-buffer, the generated
|
|
||||||
serializers, ownership storage, and the drivable loop.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — the freedom layer, a hand-rolled replication tick
|
|
||||||
entry {
|
|
||||||
while running() {
|
|
||||||
if is_server() {
|
|
||||||
for (Transform) in query [Transform, Owned] {
|
|
||||||
let n = serialize_Player(self(), buf) # compiler-generated
|
|
||||||
net_send(ALL, buf, n) # developer's transport
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let n = net_poll(buf, CAP)
|
|
||||||
if n > 0 { apply_Player(target_of(buf), buf, n) }
|
|
||||||
}
|
|
||||||
tick_render(); present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This *works*, but it is deliberately not how most games should be written — it puts
|
|
||||||
serialization and role branching in the developer's face. That is what §6 fixes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. The high-level DX layer (the default)
|
|
||||||
|
|
||||||
The developer declares **what** replicates, **who** owns, and **where** handlers
|
|
||||||
run. No serialization, no transport, no `is_server()` in ordinary code.
|
|
||||||
|
|
||||||
### 6.1 `@Sync` — what replicates, at three granularities
|
|
||||||
|
|
||||||
Replication is **opt-in at the field level**: a field crosses the wire only when it
|
|
||||||
is explicitly marked. There is no `@NoSync` — the surface is purely additive.
|
|
||||||
|
|
||||||
Two independent switches, and **both must be on** for a field to replicate:
|
|
||||||
|
|
||||||
1. **A field is *replicable*** iff it is `@Sync`-marked — directly
|
|
||||||
(`@Sync hp: int`), or via `@Sync property P { … }` (a shorthand that marks
|
|
||||||
*every* field of `P` replicable). *Only marked fields — never all-by-default.*
|
|
||||||
2. **A component *participates* in a model** iff the model marks it `@Sync`
|
|
||||||
(`@Sync Transform` inside the `model`). Participation is decided **per model
|
|
||||||
use-site**, so the same property syncs in one model and not another.
|
|
||||||
|
|
||||||
A field of an entity replicates **iff it is replicable AND its component
|
|
||||||
participates in that entity's model.**
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — the three levels
|
|
||||||
@Sync property Position { x: int, y: int } # every field of Position is replicable
|
|
||||||
property Health { @Sync hp: int, max: int } # only hp is replicable; max never is
|
|
||||||
property Transform { @Sync x: int, @Sync y: int, angle: int } # x, y replicable; angle not
|
|
||||||
|
|
||||||
@Owned model Player { # entities carry a network owner
|
|
||||||
@Sync Transform # participates → replicates x, y (not angle)
|
|
||||||
@Sync Health # participates → replicates hp (not max)
|
|
||||||
@Sync Position # participates → replicates x, y
|
|
||||||
}
|
|
||||||
|
|
||||||
model Prop { # a non-owned decoration
|
|
||||||
Transform # not @Sync here → Transform does NOT replicate — the
|
|
||||||
# "non-synced Transform sometimes" case, for free
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Checked, not silent.** `@Sync` on a `ptr`/non-POD-scalar field is a **compile
|
|
||||||
error** ("networked fields must be POD scalars" — footgun 3). A model that
|
|
||||||
`@Sync`es a component with *zero* replicable fields is a **compile warning**
|
|
||||||
(participation that replicates nothing).
|
|
||||||
- **Per-field direction** rides the same annotation as an argument, mirroring how
|
|
||||||
`@Queries(these:…, on:…)` takes args: `@Sync(to: owner) hp: int` replicates a
|
|
||||||
field only to the entity's owner (Unity's `SendToOwner`). Default is `to: all`.
|
|
||||||
|
|
||||||
### 6.2 Roles — where a handler runs
|
|
||||||
|
|
||||||
The role is a **declarative annotation on the handler**, never a runtime branch.
|
|
||||||
Unmarked code is the shared, deterministic simulation and runs everywhere.
|
|
||||||
|
|
||||||
| Annotation | Runs where | Meaning |
|
|
||||||
|---|---|---|
|
|
||||||
| *(none)* | everywhere | shared, deterministic simulation |
|
|
||||||
| **`@Server`** | the authority only | server-authoritative logic; clients receive the result via `@Sync` |
|
|
||||||
| **`@Predicted`** | the owning client (speculatively) **and** the server (authoritatively) | responsive local control, auto-reconciled against the server |
|
|
||||||
|
|
||||||
`@Predicted` is **explicit** — the developer opts an owned entity's control handlers
|
|
||||||
into prediction; the language does not silently predict. The name states the netcode
|
|
||||||
role (owner-predicts + server-authoritative + reconcile), not the machine, and
|
|
||||||
matches Unity's `GhostMode.Predicted` so the concept transfers.
|
|
||||||
|
|
||||||
`@Interpolated` — how a *non-owned* synced component is smoothed between snapshots on
|
|
||||||
a remote client — is a **presentation** concern on the component, kept separate from
|
|
||||||
these sim-handler roles rather than muddying them.
|
|
||||||
|
|
||||||
### 6.3 Ownership
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
@Owned model Player { @Sync Transform; @Sync Health } # every Player entity has a network owner
|
|
||||||
```
|
|
||||||
|
|
||||||
`@Owned` gives the model an owner slot (the `@L_owner` array); `owner(e)` /
|
|
||||||
`set_owner(e, id)` read and assign it (the authority assigns). `is_owner(e)` and
|
|
||||||
`@Predicted` dispatch read it. Ownership gates who may write `@Sync(to: owner)`
|
|
||||||
fields and who runs `@Predicted` handlers.
|
|
||||||
|
|
||||||
### 6.4 RPCs are directional remote events
|
|
||||||
|
|
||||||
RPCs are the event bus with a direction flag — no new concept:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
@ToServer event Fire { dir: int } # client → server (a request)
|
|
||||||
@ToClients event Boom { x: int, y: int } # server → clients (a broadcast)
|
|
||||||
|
|
||||||
@Server @On(Fire) handler DoFire { spawn Bullet { dir: Fire.dir } } # authority handles the request
|
|
||||||
@On(Boom) handler Vfx { spawn Explosion { x: Boom.x, y: Boom.y } } # every client reacts
|
|
||||||
```
|
|
||||||
|
|
||||||
`@ToServer`/`@ToClients` mark an `event` remote; the compiler serializes its POD
|
|
||||||
payload (already flat — [EVENTS-DESIGN EV0](EVENTS-DESIGN.md)) and routes it through
|
|
||||||
the transport seam in the declared direction, re-`emit`ting it on the far side into
|
|
||||||
the ordinary event dispatch.
|
|
||||||
|
|
||||||
### 6.5 The whole game, high-level
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — read top to bottom: you always know where each line runs
|
|
||||||
program Shooter {
|
|
||||||
@Sync property Position { x: int, y: int }
|
|
||||||
property Health { @Sync hp: int, max: int }
|
|
||||||
|
|
||||||
@Owned model Player { @Sync Position; @Sync Health }
|
|
||||||
model Bullet { Position }
|
|
||||||
|
|
||||||
handler Physics phase FixedUpdate { … } # no tag → shared, identical everywhere
|
|
||||||
@Predicted handler Move phase Input { … } # owner predicts, server authoritative
|
|
||||||
@Server handler Death phase Update { … } # authority only; clients get the result via @Sync
|
|
||||||
|
|
||||||
@ToServer event Fire { dir: int }
|
|
||||||
@Server @On(Fire) handler DoFire { spawn Bullet { … } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
No `is_server()`, no `net_send`, no serializer — yet every line's role is legible,
|
|
||||||
and every replicated field is explicitly opted in.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Lowering summary
|
|
||||||
|
|
||||||
Everything above reduces to the §5 primitives, gated so an un-networked build is
|
|
||||||
unchanged:
|
|
||||||
|
|
||||||
| High-level | Lowers to |
|
|
||||||
|---|---|
|
|
||||||
| `@Sync` field / `@Sync C` in a model | a per-model `serialize_<M>` / `apply_<M>` over the replicable-and-participating fields, + a `sync manifest` a runtime reads |
|
|
||||||
| `@Sync(to: owner)` | a field tag in the manifest; the serializer branches on `owner(e) == peer` |
|
|
||||||
| `@Owned` | an `@L_owner` array + `owner()`/`set_owner()`, like `@L_kind` |
|
|
||||||
| `@Server` / `@Predicted` handler | the handler's dispatch wrapped in a role guard the runtime's role register drives (the `rt_*` seam pattern) |
|
|
||||||
| `@ToServer` / `@ToClients event` | payload serialize + `net_send(direction, …)` at the `emit` site; inbound bytes rebuild + re-`emit` |
|
|
||||||
| `world_save`/`world_load` to buffer | the existing `save()`/`load()` snapshot machinery, retargeted from a file handle to a memory buffer |
|
|
||||||
| drivable `tick_fixed`/`tick_render` | the phase runners the compiler already generates for the frame loop, exposed as callables when a game owns its `entry` loop |
|
|
||||||
|
|
||||||
No heap, no hidden runtime beyond the honestly-named transport/role seams a
|
|
||||||
networking library fills — the same relationship windowing already has.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. The offline dividend
|
|
||||||
|
|
||||||
Because these are **opt-in-cost annotations** — serializers *generated*, nothing
|
|
||||||
*run* until a networking runtime is spliced — a build with no runtime is
|
|
||||||
**byte-identical to single-player**, and every role guard collapses to "run here."
|
|
||||||
You build the game offline, drop in a runtime, and the same annotated code starts
|
|
||||||
replicating. That is Unity's "offline mode adjustable," achieved by the same
|
|
||||||
opt-in-cost invariant the whole event system already holds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. The one genuinely hard corner
|
|
||||||
|
|
||||||
Determinism holds beautifully for `int`/`fixed` simulations, which makes lockstep
|
|
||||||
and rollback cheap. It **breaks for `f32` across platforms** — so **3D/voxel +
|
|
||||||
lockstep stays the hard corner** (3D wants floats; the Luanti analysis flagged that
|
|
||||||
`fixed` saturates at ±32768). No language sleight-of-hand fixes this; the
|
|
||||||
determinism contract (§11) states it plainly, and a developer choosing lockstep for
|
|
||||||
a 3D game has to accept it (or choose state replication, §10's other branch, where
|
|
||||||
per-frame determinism is not required).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Two model families, both reachable — neither built in
|
|
||||||
|
|
||||||
The language commits to **neither**; both are library policy over the §5 primitives.
|
|
||||||
|
|
||||||
- **Deterministic lockstep / rollback** — exchange only inputs; reproduce the sim;
|
|
||||||
on misprediction, `world_load` a snapshot and re-`tick_fixed`. Plays to Ludic's
|
|
||||||
determinism, and GGPO-cheap because snapshot/restore already exists. Best for
|
|
||||||
2D/integer/fixed games.
|
|
||||||
- **State replication** — the authority `world_save`s (or per-`@Sync` serializes),
|
|
||||||
delta-encodes against the last acked snapshot per peer, ships the diff; peers
|
|
||||||
`apply_*` it and interpolate/predict. Heavier, but needed when the sim can't be
|
|
||||||
deterministic (float physics, 3D).
|
|
||||||
|
|
||||||
A **blessed reference runtime** (§13, N6) can ship one of these so `@Sync` games
|
|
||||||
work out of the box — the way [`tests/mod_c/mod.c`](tests/mod_c/mod.c) proved the
|
|
||||||
event ABI — while the seams stay open for others.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. The determinism contract (what the language must guarantee)
|
|
||||||
|
|
||||||
For a developer to *trust* lockstep, the language must promise, document, and where
|
|
||||||
possible *enforce*:
|
|
||||||
|
|
||||||
1. **`fixed`/`int` math is bit-identical across platforms.** The networked sim must
|
|
||||||
avoid `f32` (footgun 2). *(Enforcement: at least a documented rule; ideally a
|
|
||||||
`@Sync`/`@Server`-reachable-code float lint.)*
|
|
||||||
2. **ECS iteration order is stable** — query order is declaration/id order, and
|
|
||||||
EV6 already fixes event-dispatch order. No hash-map iteration in the sim path.
|
|
||||||
3. **RNG is deterministic from a shared seed** — `seed()` exists; the seed must be
|
|
||||||
synchronized at session start (library policy) and never re-seeded from
|
|
||||||
wall-clock mid-sim.
|
|
||||||
4. **Networked components are POD scalars** — no `ptr`/heap fields cross the wire
|
|
||||||
(footgun 3). *Enforced:* `@Sync` on a non-scalar field is a compile error.
|
|
||||||
5. **Entity ids agree across peers** — lockstep gets this free from determinism;
|
|
||||||
replication needs an id-mapping table (library policy).
|
|
||||||
|
|
||||||
This contract is the language's real networking responsibility. Most of it is
|
|
||||||
*already true*; the work is stating and enforcing it, not inventing it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Design principles
|
|
||||||
|
|
||||||
1. **Mechanism in the language, policy in the library.** Expose serializers,
|
|
||||||
transport seam, ownership, snapshot, drivable sim. Never bake in authority,
|
|
||||||
prediction, or matchmaking.
|
|
||||||
2. **Role is declared, not branched.** `@Server`/`@Predicted` on handlers; unmarked
|
|
||||||
code runs everywhere. No `is_server()` in ordinary gameplay.
|
|
||||||
3. **Replication is explicit and opt-in.** Only `@Sync`-marked fields cross the
|
|
||||||
wire; participation is decided per model. Nothing replicates by surprise.
|
|
||||||
4. **Opt-in cost.** Un-networked builds are byte-identical; the sim runs offline
|
|
||||||
with the same code.
|
|
||||||
5. **Determinism is a promise the language keeps.** Enforce the POD-scalar rule;
|
|
||||||
document the float/iteration/seed rules; keep the sim reproducible.
|
|
||||||
6. **Two altitudes, always.** The high-level lowers to primitives that stay
|
|
||||||
callable. The sugar is the default; the freedom layer is never removed.
|
|
||||||
7. **Reuse, don't reinvent.** Snapshot = generalized `save()`; RPC = directional
|
|
||||||
`event`; serializer = generated from the EV2 schema; role seam = the `rt_*`
|
|
||||||
pattern. Networking is the fourth lens, not a parallel stack.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Suggested implementation order
|
|
||||||
|
|
||||||
Each phase is independently shippable and testable, matching how the repo phases
|
|
||||||
work (and how EVENTS-DESIGN sequenced EV0–EV7).
|
|
||||||
|
|
||||||
- **N0 — transport seam + loopback. ✅ SHIPPED.** The `net_send`/`net_poll` extern
|
|
||||||
seam and a loopback host stub; an echo test. The floor; needed almost no compiler
|
|
||||||
work — just finishing `extern fn`: a call lowers to a direct `@<sym>` call and the
|
|
||||||
header emits a matching `declare`, so any C/Rust/Zig library (a socket, here the
|
|
||||||
loopback) binds through the same seam windowing uses. `find_extern` (emit_core),
|
|
||||||
the extern branch in emit_expr's call path, `emit_extern_decls` (emit_head).
|
|
||||||
([`examples/networking/net_echo.ludic`](examples/networking/net_echo.ludic),
|
|
||||||
[`tests/net_c/loopback.c`](tests/net_c/loopback.c) → `4 10 20 30 42`.)
|
|
||||||
- **N1 — snapshot-to-buffer. ✅ SHIPPED.** Generalized `save()`/`load()` to a memory
|
|
||||||
buffer: `world_size()` (exact snapshot bytes), `world_save(buf) -> int`,
|
|
||||||
`world_load(buf, len)`. The same fixed block list (entity count, freelist, alive,
|
|
||||||
kind, vars, per-component `@S_`/`@H_`) now feeds a file (fwrite/fread) *or* a buffer
|
|
||||||
(memcpy over a threaded i64 offset), chosen by `g_snap_mode` in emit_save.ludic;
|
|
||||||
no rt_ hook (the ECS world only). The rollback/replication substrate.
|
|
||||||
([`examples/networking/net_snapshot.ludic`](examples/networking/net_snapshot.ludic),
|
|
||||||
[`tests/net_c/snapshot_mod.c`](tests/net_c/snapshot_mod.c) → `50 7 50`.)
|
|
||||||
- **N2 — `@Sync` codegen. ✅ SHIPPED.** The three-level annotations → generated
|
|
||||||
per-model `serialize_<M>`/`apply_<M>` + by-kind dispatchers (`ludic_serialize`/
|
|
||||||
`apply`/`sync_size`, and the `serialize`/`apply`/`sync_size` builtins); the
|
|
||||||
POD-scalar compile error and the empty-participation warning. The declarative
|
|
||||||
core. ([`examples/networking/net_sync.ludic`](examples/networking/net_sync.ludic) → `12 3 4 50 999`,
|
|
||||||
emit in [`selfhost/emit_net.ludic`](selfhost/emit_net.ludic).)
|
|
||||||
- **N3 — ownership. ✅ SHIPPED.** `@Owned` + the `@L_owner_arr` array +
|
|
||||||
`owner()`/`set_owner()`/`is_owner()`; owners are part of the world snapshot.
|
|
||||||
([`examples/networking/net_owner.ludic`](examples/networking/net_owner.ludic) → `-1 7 0 1`.)
|
|
||||||
- **N4 — remote events (RPCs). ✅ SHIPPED.** `@ToServer`/`@ToClients` on `event`s →
|
|
||||||
payload serialize (`[event id][fields]`) + directional `net_send` + `net_pump()`
|
|
||||||
far-side re-`emit`. ([`examples/networking/net_rpc.ludic`](examples/networking/net_rpc.ludic) → `0 8`.)
|
|
||||||
- **N5 — roles + drivable sim. ✅ SHIPPED.** `@Server`/`@Predicted` role-guarded
|
|
||||||
dispatch driven by the `@L_role` register (`set_role`/`is_server`/`local_id`);
|
|
||||||
the opt-in `entry`-owns-the-loop with `tick_fixed()`/`tick_render()`. Together
|
|
||||||
these let prediction/rollback be written in developer/library code.
|
|
||||||
([`examples/networking/net_roles.ludic`](examples/networking/net_roles.ludic) → `1 102`.)
|
|
||||||
- **N6 — a blessed reference netcode runtime. ✅ SHIPPED.** A Ludic library
|
|
||||||
([`examples/networking/net_rt.ludic`](examples/networking/net_rt.ludic)) — server-authoritative state
|
|
||||||
replication over the primitives — plus a full end-to-end demo, proving the seams
|
|
||||||
the way the C mod proved the event ABI, but in pure Ludic over the built-in
|
|
||||||
transport. Library policy, swappable for lockstep+rollback.
|
|
||||||
([`examples/networking/net_demo.ludic`](examples/networking/net_demo.ludic) → `5 999 5`.) A built-in
|
|
||||||
loopback transport (N0) means all of this needs **no foreign code at all**.
|
|
||||||
|
|
||||||
N0–N2 deliver "state can be declared, serialized, and moved." N3–N4 add ownership
|
|
||||||
and RPCs. N5 unlocks prediction. N6 is a batteries-included default that others can
|
|
||||||
replace. The **determinism contract (§11)** is cross-cutting — documented from N0,
|
|
||||||
enforced incrementally.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Open decisions
|
|
||||||
|
|
||||||
1. **Field direction vocabulary.** `@Sync(to: owner)` / `@Sync(to: all)` confirmed
|
|
||||||
in spirit; is `to:` the right key, and do we also want `to: server` (a field only
|
|
||||||
the authority reads)? How does per-field direction interact with `@Predicted`?
|
|
||||||
2. **Blessed runtime, or seams only?** Events chose "seams + reference mod, bless
|
|
||||||
nothing." Networking's DX may justify shipping one reference runtime (N6). One,
|
|
||||||
or none?
|
|
||||||
3. **Authority default.** Server-authoritative with `@Predicted` opt-in is the safe,
|
|
||||||
Unity-ish default. Confirm, or keep the language authority-neutral and leave even
|
|
||||||
that to the runtime?
|
|
||||||
4. **Drivable loop shape.** Whole-frame `tick()` vs the `tick_fixed()`/`tick_render()`
|
|
||||||
split; how a developer-owned `entry` loop coexists with scenes, the `rt_*` hooks,
|
|
||||||
and the auto-loop (opt-in via presence of an `entry` block?).
|
|
||||||
5. **Snapshot granularity.** Full `world_save` vs per-`@Sync` serialize vs a
|
|
||||||
generated delta between two snapshots — which does the language provide, and which
|
|
||||||
is library work?
|
|
||||||
6. **Float determinism enforcement.** A documented rule only, or a real lint that
|
|
||||||
flags `f32` reachable from `@Server`/`@Predicted`/`@Sync` code paths?
|
|
||||||
7. **Ownership at component granularity.** Unity's DOTS allows per-component owner
|
|
||||||
send-rules. Is `@Owned` per-*entity* enough, or do we need per-component owners
|
|
||||||
(a real complexity jump)?
|
|
||||||
8. **Networking substrate for the remote half of EVENTS EV7.** This doc's directional
|
|
||||||
remote events (N4) *are* the local/remote split EVENTS-DESIGN EV7 deferred for
|
|
||||||
"no networking substrate." N4 is that substrate — the two docs meet here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Companion to [EVENTS-DESIGN.md](EVENTS-DESIGN.md) (remote events are directional
|
|
||||||
events; serializers reuse the EV2 world-table schema; EV7's deferred local/remote
|
|
||||||
split lands here as N4), [LIFECYCLE-DESIGN.md](LIFECYCLE-DESIGN.md) (`@OnChange`/LC2
|
|
||||||
is the dirty-tracking primitive for delta replication), and
|
|
||||||
[SCENES-DESIGN.md](SCENES-DESIGN.md). Supersedes nothing until the compiler work in
|
|
||||||
§13 lands.*
|
|
||||||
286
README.md
286
README.md
|
|
@ -1,170 +1,200 @@
|
||||||
# Ludic
|
# Ludic
|
||||||
|
|
||||||
Ludic is an **ahead-of-time compiled** language for 2D games with an
|
A compiled language for 2D games. The entity-component system is part of the
|
||||||
entity-component core, a deterministic fixed-point runtime, and its graphics
|
syntax, the runtime is deterministic fixed-point, and `ludicc` lowers Ludic
|
||||||
stack built into the language. `ludicc` lowers Ludic straight to LLVM IR and
|
straight to LLVM IR — **no C is generated, compiled or linked in a build.**
|
||||||
emits a native binary — and **`ludicc` is itself written in Ludic**, compiles
|
|
||||||
its own source to a byte-exact fixpoint, and rebuilds from a checked-in IR seed
|
|
||||||
with **no C compiler in the loop**.
|
|
||||||
|
|
||||||
```
|
The compiler is written in Ludic. It compiles its own source to a byte-exact
|
||||||
.ludic ──► ludicc ──► LLVM IR ──► object ──► native binary
|
fixpoint and rebuilds from a checked-in IR seed with clang alone; CI asserts
|
||||||
(in Ludic)
|
that on every push.
|
||||||
|
|
||||||
|
- **Documentation:** <https://workshopsoft.pages.workshopsoft.io/ludic/>
|
||||||
|
- **API reference:** <https://workshopsoft.pages.workshopsoft.io/ludic/api.html>
|
||||||
|
- **Issues:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Hello {
|
||||||
|
|
||||||
|
property Position { column: int = 0, row: int = 0 }
|
||||||
|
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||||
|
|
||||||
|
handler SpawnEnemies phase Start {
|
||||||
|
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
|
||||||
|
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
# a handler declares the entities it touches; the body runs
|
||||||
|
# once per match, with each property bound by name.
|
||||||
|
@Queries(these: [Position, Velocity])
|
||||||
|
handler AdvancePositions phase FixedUpdate {
|
||||||
|
Position.column += Velocity.delta_x
|
||||||
|
Position.row += Velocity.delta_y
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**No C is generated, compiled or linked in a build.** No interpreter, no
|
## Getting started
|
||||||
transpiler, no C runtime: the framebuffer, sprites, PNG/DEFLATE decoding,
|
|
||||||
TrueType text, the retained UI, the registers and the RNG are all written in
|
|
||||||
Ludic (`runtime/native/*.ludic`); only the window seam — five `win_*` functions
|
|
||||||
— is hand-written LLVM IR against the platform ABI (`runtime/native/cocoa.ll`),
|
|
||||||
the same floor Rust and Swift stand on.
|
|
||||||
|
|
||||||
## Backends
|
Install the toolchain — the compiler, the `ludic` CLI, the engine runtime, the
|
||||||
|
formatter and the language server — with one command:
|
||||||
| Backend | Status |
|
|
||||||
|---|---|
|
|
||||||
| **Native 2D** (macOS/Cocoa window; headless render for CI) | **Shipping** — the default `bin/x app` target. |
|
|
||||||
| **Web / wasm32** | **In progress.** The browser platform layer is in-tree and documented — `runtime/web/` (the `<canvas>` window `platform.js`, the libc-free `wasm.ll` floor) and a Node harness that diffs native vs. wasm frame-for-frame (`tools/ludic-web/run.mjs`). Emitting wasm was a capability of the retired C compiler and is **not yet re-wired on the self-hosted toolchain**; see [COMPILING.md](COMPILING.md). |
|
|
||||||
|
|
||||||
The same is true of `--target` cross-compilation and `--shared` libraries: both
|
|
||||||
are designed and documented, both lived in the old C compiler, and both are
|
|
||||||
pending re-implementation on the self-hosted native toolchain.
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
`bin/x` is the project's task runner — one native binary, written in Ludic and
|
|
||||||
compiled by Ludic, that replaces every build/test/bootstrap shell script.
|
|
||||||
Bootstrap it once from a clean checkout (the only step Ludic can't do for
|
|
||||||
itself, since compiling Ludic needs a compiler) with clang alone:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build the whole toolchain and run a game:
|
It installs into `~/.ludic` and puts `~/.ludic/bin` on your `PATH` in every
|
||||||
|
shell — the PATH line lives in `~/.ludic/env`, sourced from `~/.profile`,
|
||||||
|
`~/.zshenv` and your bash or fish config. Nothing else on the machine is touched;
|
||||||
|
uninstalling is `rm -rf ~/.ludic` and deleting those two-line blocks. Where a
|
||||||
|
prebuilt toolchain exists for your platform it is downloaded and verified against
|
||||||
|
a published checksum; where it does not, the installer bootstraps from the
|
||||||
|
compiler's own IR seed with clang. Either way you need clang (or Xcode's Command
|
||||||
|
Line Tools) to link, since Ludic emits LLVM IR and links it natively.
|
||||||
|
|
||||||
|
Then make a game:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x build # -> bin/{ludicc,ludic,x,ludic-fmt,ludic-lsp}
|
ludic new mygame
|
||||||
bin/x app examples/games/snake.ludic # compile + open a native window
|
cd mygame
|
||||||
./build/snake
|
ludic run # compiles src/main.ludic and opens a native window
|
||||||
```
|
```
|
||||||
|
|
||||||
Render a frame headlessly (what CI checks) — output lands in `build/`, never the
|
`ludic new` writes a manifest, a program that already moves something on screen,
|
||||||
repo root:
|
and a test. `ludic build` stops at the binary; `ludic bundle` goes on to the
|
||||||
|
thing you can actually give someone. Rendering is deterministic, so a frame can
|
||||||
|
be produced without a window, which is what CI diffs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x app examples/games/chronorift.ludic --headless
|
ludic test
|
||||||
mkdir -p build && printf 'ddddwww' | ./build/chronorift_headless # writes build/out.ppm
|
ludic build --headless
|
||||||
sips -s format png build/out.ppm --out frame.png
|
printf 'ddddwww' | ./build/mygame_headless # writes build/out.ppm
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the suites:
|
`ludic help` lists every command, and `ludic doctor` checks the install.
|
||||||
|
[`examples/`](examples/README.md) is a tour grouped by intent: games, rendering,
|
||||||
|
ECS, events, networking, language features and the standard library — compile any
|
||||||
|
of them with `ludic build examples/games/snake.ludic`.
|
||||||
|
|
||||||
|
### Building from a checkout
|
||||||
|
|
||||||
|
Contributors also get `ludic-dev`, a second binary carrying the toolchain's own
|
||||||
|
tasks — building the compiler, the suites, the docs site, releases. It is built
|
||||||
|
from a checkout and is not part of an install, so nothing a user runs is mixed
|
||||||
|
up with it. Bootstrapping is the only step Ludic cannot do for itself, since
|
||||||
|
compiling Ludic needs a compiler — clang assembles the checked-in IR seed, and
|
||||||
|
that compiler builds the rest:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/x test # full regression: compiler builds from seed, every example, golden renders
|
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||||
bin/x selfhost-test # correctness + the self-hosting / C-free bootstrap fixpoints
|
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||||
bin/x help # every command
|
bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp}
|
||||||
|
bin/ludic-dev test # the regression suite
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layout
|
## The language
|
||||||
|
|
||||||
| Path | What it is |
|
- **ECS in the syntax.** `property`, `model` and `handler` are keywords. Query
|
||||||
|------|-----------|
|
with `for (a, b) in query [A, B, {Tag}] where <expr> { … }`; `spawn` and
|
||||||
| [`selfhost/*.ludic`](selfhost/) | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Built from `selfhost/ludicc.seed.ll` with clang alone. |
|
`despawn` recycle entity slots; `@`-annotations drive lifecycle hooks.
|
||||||
| [`selfhost/golden/renders.sha256`](selfhost/golden/renders.sha256) | text baseline of render-output hashes (replaces binary `.ppm` fixtures); regenerate with `bin/x golden`. |
|
- **Deterministic by construction.** Q16.16 `fixed` arithmetic and a seeded RNG
|
||||||
| [`tools/x/*.ludic`](tools/x/) | **the task runner, written in Ludic** — one binary (`bin/x`) that builds, tests, bootstraps and reseeds the project, replacing every shell script. |
|
give the same frame byte-for-byte on every run — the basis for replays,
|
||||||
| [`runtime/native/`](runtime/native/) | the runtime **in Ludic** for the native path: `core` (framebuffer, input, RNG), `image`/`inflate` (PNG + DEFLATE, no zlib), `truetype` (glyph rasterizer), `ui` (retained widget tree); plus `cocoa.ll`, the macOS window seam in LLVM IR. |
|
lockstep netcode and golden-image tests.
|
||||||
| [`runtime/web/`](runtime/web/) | the browser platform layer: `platform.js` (the `<canvas>` window), `wasm.ll` (the libc-free floor), `index.html`. |
|
- **Scenes and state machines.** `scene` / `layer` / `become` model
|
||||||
| [`examples/`](examples/README.md) | the example tour, grouped by intent — `games/`, `rendering/`, `ecs/`, `events/`, `networking/`, `lang/`, `library/`. See [examples/README.md](examples/README.md). |
|
mutually-exclusive game states with enter and exit hooks; `match` / `machine`
|
||||||
| [`tools/ludic-tools/`](tools/ludic-tools/) | the editor toolchain **in Ludic**: `ludic-fmt` (formatter) and `ludic-lsp` (language server) — one lexer, one vocabulary shared by both. |
|
/ `state` handle dispatch and per-entity FSMs.
|
||||||
| [`tools/editors/`](tools/editors/README.md) | plugins for VS Code and JetBrains, plus config for Neovim, Helix, Emacs, Sublime and Zed. |
|
- **Events and networking.** A cancellable event bus (`event` / `emit` / `@On`)
|
||||||
| [`docs/`](docs/) | the per-symbol API reference, regenerated into the docs site. |
|
and networking primitives (`@Sync`, ownership, RPCs) over a built-in transport.
|
||||||
| [`COMPILING.md`](COMPILING.md) | the native pipeline: `ludicc → LLVM IR → exe`, the `rt_*` runtime protocol, and the (pending) wasm/cross-compile/shared-library paths. |
|
- **Batteries in the language.** Framebuffer primitives, PNG sprites, TrueType
|
||||||
|
text and a retained `ui` widget tree declared as data, plus a namespaced
|
||||||
|
standard library (`Math`, `Text`, `List`, `Random`, `Crypto`, `Tiled`, …).
|
||||||
|
- **Whole-world snapshots.** `save()` and `load()` serialize every entity,
|
||||||
|
property and program `var` in one call.
|
||||||
|
|
||||||
Design and roadmap documents — `LANGUAGE.md`, `EVENTS-DESIGN.md`,
|
[LANGUAGE.md](LANGUAGE.md) is the full reference; the
|
||||||
`NETWORKING-DESIGN.md`, `SCENES-DESIGN.md`, `LIFECYCLE-DESIGN.md`,
|
[API reference](https://workshopsoft.pages.workshopsoft.io/ludic/api.html)
|
||||||
`SYNTAX-REDESIGN.md`, `MOBILE-DESIGN.md`, `LUANTI-ROADMAP.md`, `BOOTSTRAP.md` —
|
documents every symbol on its own page.
|
||||||
live at the repository root today and are being migrated to the wiki.
|
|
||||||
|
|
||||||
## Language at a glance
|
## Shipping
|
||||||
|
|
||||||
- `program` / `property` (typed fields + defaults) / `model` (named entity kinds)
|
A built binary is a program, not an application: it opens its assets by a path
|
||||||
/ `system` (`phase`, `@annotations`, `reads`/`writes`).
|
relative to the working directory, so it runs from the project root and nowhere
|
||||||
- ECS queries `for (a, b) in query [A, B, {Tag}] where <expr> { … }`,
|
else, and it wears the generic executable icon.
|
||||||
`spawn`/`despawn` with slot reuse, `@`-driven lifecycle hooks.
|
|
||||||
- An **event bus** (`event` / `emit` / `@On`, cancellable, `@Public` promotion)
|
|
||||||
and **networking** primitives (`@Sync`, ownership, RPCs) over a built-in
|
|
||||||
loopback transport — all deterministic, all pure Ludic.
|
|
||||||
- `scene` / `layer` / `become`, `match` / `machine` + `state`.
|
|
||||||
- Types `int`, `fixed` (Q16.16), `bool`, `entity`, `str`, `byte`, typed buffers;
|
|
||||||
a growing namespaced **standard library** (`Math`, `Vector`, `Time`/`Date`/
|
|
||||||
`Duration`/`Clock`, `Random`, `Hash`, `Crypto`, sorting, …).
|
|
||||||
- Deterministic seeded RNG and `save()`/`load()` snapshot of the whole World.
|
|
||||||
- Built-in 2D: framebuffer primitives, PNG sprites, TrueType text, 9-slice, and
|
|
||||||
a retained `ui` widget tree declared as data.
|
|
||||||
|
|
||||||
See [LANGUAGE.md](LANGUAGE.md) for the full reference, and
|
```bash
|
||||||
[examples/README.md](examples/README.md) for runnable demos of each feature.
|
ludic pack # every asset the game opens, into one .lpak
|
||||||
|
ludic bundle # ...and that, the binary, an icon and the metadata, as a .app
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing about how the game is written changes. `gltf_load("assets/kit/hiker",
|
||||||
|
…)` reads a file during development and a run of bytes inside the bundle once
|
||||||
|
shipped, and cannot tell which — the pack is spliced in at `file_open`, the one
|
||||||
|
place every asset in a Ludic program comes through. A bundled game also gets a
|
||||||
|
boot splash it controls (`App.splash_hide()`) and a writable home under
|
||||||
|
Application Support, because Finder starts a `.app` at `/` where no save could
|
||||||
|
be written.
|
||||||
|
|
||||||
|
Without a pack beside it — which is every `ludic run` — nothing mounts and every
|
||||||
|
open goes to the filesystem exactly as before. See [docs/SHIPPING.md](docs/SHIPPING.md).
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
Dependencies are identified by URL, resolved with minimal version selection, and
|
||||||
|
cached in a content-addressed store:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
||||||
|
ludic get # install from package.ludic, write the lock
|
||||||
|
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
|
||||||
|
ludic verify # check locked packages against the store
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ludic.*` packages — canonical ECS components, the gameplay, platformer,
|
||||||
|
RPG, shooter and NPC-AI modules — ship with the toolchain, so importing one needs
|
||||||
|
no fetch step at all.
|
||||||
|
|
||||||
|
See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest and lockfile model.
|
||||||
|
|
||||||
## Editor support
|
## Editor support
|
||||||
|
|
||||||
```bash
|
Editors spawn `ludic lsp`; the server ships with the toolchain, so there is
|
||||||
bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp
|
nothing extra to install. It speaks LSP 3.17 over stdio, so one binary serves
|
||||||
```
|
every editor: completion, diagnostics from the compiler itself, go-to-definition
|
||||||
|
and rename across imports, and comment-preserving formatting. `ludic fmt` runs
|
||||||
|
the same formatter as a CLI, for pre-commit hooks. Both understand
|
||||||
|
```` ```ludic ```` fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim,
|
||||||
|
Helix, Emacs, Sublime and Zed are in [`tools/editors/`](tools/editors/README.md).
|
||||||
|
|
||||||
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
|
## Status
|
||||||
context-aware completion, diagnostics from the compiler itself,
|
|
||||||
go-to-definition and rename across `import`ed files, and comment-preserving
|
|
||||||
formatting. `ludic-fmt` is the same formatter as a CLI, for pre-commit hooks and
|
|
||||||
CI. Both also understand ```` ```ludic ```` fences in Markdown. Plugins and
|
|
||||||
drop-in config are in [`tools/editors/`](tools/editors/README.md).
|
|
||||||
|
|
||||||
## Chrono Rift — the flagship game
|
The native 2D backend ships: a Cocoa window on macOS, a headless renderer for
|
||||||
|
CI, and the whole runtime — framebuffer, PNG/DEFLATE decoding, TrueType
|
||||||
|
rasterizer, retained UI, RNG — written in Ludic under
|
||||||
|
[`runtime/native/`](runtime/native/). Only the window seam (`win_*`: window,
|
||||||
|
keys, mouse, cursor, gamepad, touch) is hand-written LLVM IR against the
|
||||||
|
platform ABI, the same floor Rust and Swift stand on.
|
||||||
|
|
||||||
[`examples/games/chronorift.ludic`](examples/games/chronorift.ludic) is a
|
The **web/wasm32 backend is not currently available.** The browser platform
|
||||||
playable co-op JRPG — overworld, dungeon, random encounters, a turn-based co-op
|
layer is in-tree under [`runtime/web/`](runtime/web/), but emitting wasm was a
|
||||||
battle, a boss, an item shop and snapshot save/load — split across modules under
|
capability of the retired C compiler and has not been re-wired on the
|
||||||
[`games/chronorift/`](examples/games/chronorift/). Its art is CC0
|
self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are
|
||||||
[Kenney](https://kenney.nl) sprites, decoded from PNG at runtime by the
|
in the same position. See [COMPILING.md](COMPILING.md).
|
||||||
Ludic-written PNG/DEFLATE decoder — no zlib, no external dependency.
|
|
||||||
|
|
||||||
- **Overworld:** `WASD` move, `K` save, `L` load.
|
Releases follow SemVer and are cut from changesets by `ludic-dev release`, then built
|
||||||
- **Battle (local co-op):** P1/Knight `W`/`S` select, `Space` confirm;
|
and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md).
|
||||||
P2/Mage `I`/`K` select, `J` confirm.
|
|
||||||
|
|
||||||
## Status & roadmap
|
|
||||||
|
|
||||||
The compiler self-hosts to a byte-exact fixpoint and rebuilds from its IR seed
|
|
||||||
with no C compiler; the ECS runtime, windowed + headless 2D rendering, the event
|
|
||||||
bus, the deterministic networking stack, scenes, and save/load are all in place
|
|
||||||
and covered by `bin/x test`. CI gates every push and PR on the build, the test
|
|
||||||
suites, and that C-free fixpoint. The toolchain is versioned with SemVer
|
|
||||||
(`ludicc --version`); releases and the `CHANGELOG.md` are cut from changesets by
|
|
||||||
`x release`.
|
|
||||||
|
|
||||||
Active work and proposals — the standard library, a fuller type system,
|
|
||||||
rendering/animation/lighting extras, input, filesystem/IO, testing, and
|
|
||||||
re-wiring the web/wasm and cross-compile backends — are tracked as issues, not
|
|
||||||
inlined here:
|
|
||||||
|
|
||||||
- **Issues & proposals:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
|
|
||||||
- **Docs site (API reference):** <https://workshopsoft.pages.workshopsoft.io/ludic/>
|
|
||||||
- **Wiki (design & roadmap):** <https://git.workshopsoft.io/workshopsoft/ludic/wiki>
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development loop
|
[CONTRIBUTING.md](CONTRIBUTING.md) covers the development loop, the commit and
|
||||||
(`bin/x reseed` → `bin/x bootstrap-cfree` → `bin/x test`), the code and commit
|
code conventions, how the bootstrap fixpoint works, and what a self-hosted CI
|
||||||
conventions, and how the bootstrap fixpoint works. Issue and pull-request
|
runner needs. Issue and pull-request templates are under
|
||||||
templates live under [`.forgejo/`](.forgejo/).
|
[`.forgejo/`](.forgejo/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The Ludic compiler and runtime source are licensed under the
|
The compiler and runtime are licensed under the
|
||||||
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`) — a
|
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`).
|
||||||
permissive license with an explicit patent grant.
|
|
||||||
|
|
||||||
The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is
|
The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is
|
||||||
third-party and released under **CC0 1.0** (public domain); each pack keeps its
|
third-party and released under **CC0 1.0**; each pack keeps its own
|
||||||
own `License.txt`. Code and assets are licensed separately: Apache-2.0 covers
|
`License.txt`. Code and assets are licensed separately — Apache-2.0 covers the
|
||||||
the source, not the art.
|
source, not the art.
|
||||||
|
|
|
||||||
354
SCENES-DESIGN.md
354
SCENES-DESIGN.md
|
|
@ -1,354 +0,0 @@
|
||||||
# Scenes, expanded — a design doc
|
|
||||||
|
|
||||||
> **Status: S0 shipped; S1–S6 are design.** The base construct — `scene` /
|
|
||||||
> `layer` / `on enter` / `on exit` / `become`, lowered to the implicit machine of
|
|
||||||
> §3 and §9 — is implemented and tested ([`examples/lang/scenes.ludic`](examples/lang/scenes.ludic),
|
|
||||||
> a `bin/x test` check). The extensions in §4–§8 (scene-owned entities, richer
|
|
||||||
> layers, the overlay stack, scene-local state, transition parameters) are still
|
|
||||||
> design targets. This document reaches deliberately past the thin sketch so we
|
|
||||||
> can decide the shape before building each one. §11 lists the open decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Where we are
|
|
||||||
|
|
||||||
A Ludic program is almost always several mutually-exclusive states — a title
|
|
||||||
screen, the overworld, a battle, a pause menu. Two ways to write that exist in
|
|
||||||
the language today, and a third is sketched:
|
|
||||||
|
|
||||||
| Approach | Status | Cost |
|
|
||||||
|---|---|---|
|
|
||||||
| Mode register consulted at the top of every handler (`if reg(R_MODE) == …`) | works | a guard re-read per handler per frame; state is a magic number; nothing scopes to it |
|
|
||||||
| `machine`/`state`/`become` over a register | works | dispatch on the register each frame; still one flat register, no per-state handlers or lifecycle |
|
|
||||||
| `scene`/`layer`/`on enter`/`on exit` | **sketch only** | — |
|
|
||||||
|
|
||||||
The sketch ([`examples/lang/scenes.ludic`](examples/lang/scenes.ludic)) specs:
|
|
||||||
|
|
||||||
- Exactly **one scene active**; the `start` scene runs first.
|
|
||||||
- A scene's handlers run only while it is active; handlers outside any scene are
|
|
||||||
global.
|
|
||||||
- **Layers group handlers; declaration order is draw order** — within a phase,
|
|
||||||
globals first, then the active scene's layers in written order.
|
|
||||||
- `on enter` / `on exit` are lifecycle hooks (not phases).
|
|
||||||
- `become Name` runs the old scene's `on exit`, switches, runs the new `on enter`
|
|
||||||
— two direct calls and a store, no dispatch table.
|
|
||||||
|
|
||||||
That's a good spine. The problem is it's specced as **sugar over a mode
|
|
||||||
register**: it tidies the syntax but adds little the register didn't already
|
|
||||||
have. The compiler knows *much* more at a scene boundary than a register does,
|
|
||||||
and this doc is about spending that knowledge.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Design principles
|
|
||||||
|
|
||||||
1. **The scene boundary is a compile-time fact — use it.** The set of handlers,
|
|
||||||
layers, and owned state for each scene is known statically. Transitions should
|
|
||||||
be direct calls and a single store, never a table walk. (The sketch already
|
|
||||||
promises this; the extensions must preserve it.)
|
|
||||||
2. **Structure, not registers.** Anything you'd track with a hand-managed
|
|
||||||
register alongside the mode — which entities belong to this state, which layers
|
|
||||||
are drawn, what's paused — should be expressible *as* scene structure and
|
|
||||||
enforced by the compiler.
|
|
||||||
3. **Reuse the machinery we already have.** Layers pausing, scenes tearing down
|
|
||||||
their entities, and hooks firing are all expressible in terms of
|
|
||||||
`enable`/`disable` (cheap flag flips), `despawn`, and the lifecycle-hook
|
|
||||||
lowering. Scenes should *compose* those, not introduce a parallel runtime.
|
|
||||||
4. **One active-scene path stays hot; overlays are the exception, not the rule.**
|
|
||||||
The common case (one full-screen scene at a time) must lower to the cheapest
|
|
||||||
possible dispatch. Richer shapes (a pause menu over a frozen world) are opt-in
|
|
||||||
and pay only for what they use.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Core model (firmed up from the sketch)
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative
|
|
||||||
scene Title start {
|
|
||||||
on enter { ui_open(UI_Menu) }
|
|
||||||
on exit { ui_visible(UI_Menu, 0) }
|
|
||||||
|
|
||||||
layer Main {
|
|
||||||
handler Choose phase Update {
|
|
||||||
if ui_clicked(UI_NewGame) { become Overworld }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene Overworld {
|
|
||||||
on enter { spawn_party() }
|
|
||||||
layer World { handler Move phase Update { … } }
|
|
||||||
layer Hud { handler Draw phase Render { … } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Unchanged from the sketch, made precise:
|
|
||||||
|
|
||||||
- **Scenes number themselves by declaration order**, exactly like `machine`
|
|
||||||
states — `Title` is `0`, `Overworld` is `1`. The active scene lives in one
|
|
||||||
implicit register (`__scene`). This makes `scene` a `machine` the compiler
|
|
||||||
writes for you, which is the right mental model and the right lowering.
|
|
||||||
- **A layer handler may not use phase `Start`.** `Start` runs once at boot,
|
|
||||||
before any scene is entered; scene setup goes in `on enter`.
|
|
||||||
- **Global handlers still run every frame**, before any scene's layers, in every
|
|
||||||
phase. A scene's layers run only while it is active.
|
|
||||||
|
|
||||||
Everything below is new.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Extension E1 — scene-owned entities (scoped lifetime)
|
|
||||||
|
|
||||||
The single biggest thing a mode register cannot do: **own the entities that only
|
|
||||||
make sense in this state, and tear them down automatically on exit.** Today a
|
|
||||||
battle scene spawns combatants in `on enter` and must remember to despawn every
|
|
||||||
one in `on exit` — miss one and it leaks into the overworld.
|
|
||||||
|
|
||||||
Proposal: entities spawned *by a scene's handlers or `on enter`* are tagged with
|
|
||||||
that scene, and `on exit` despawns them by default.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
scene Battle {
|
|
||||||
on enter { spawn Foe; spawn Foe; spawn Foe } # tagged @Battle
|
|
||||||
# on exit: implicit `despawn all @Battle` — no manual cleanup
|
|
||||||
layer World { handler Fight phase Update { … } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Implemented as an implicit **scene tag** (a `{Battle}`-style kind bit) added at
|
|
||||||
`spawn` time while a scene is active, plus a generated `despawn`-by-tag in the
|
|
||||||
synthesized `on exit`. Reuses the existing tag-filter and despawn-hook
|
|
||||||
machinery — no new runtime.
|
|
||||||
- **Opt out** for entities that should outlive the scene: `spawn Foe persist` (or
|
|
||||||
spawn it from a global handler). Persisted entities keep their data across the
|
|
||||||
transition, matching how `disable` keeps field data.
|
|
||||||
- Composes with `@OnDespawn(Model)`: the destructor hook fires for each
|
|
||||||
scene-owned entity as it's torn down, so `drop_loot`-style cleanup still runs.
|
|
||||||
|
|
||||||
**Open:** does a re-`become Battle` get fresh entities (fresh tag generation) or
|
|
||||||
resume the old ones? Default: fresh. See §9.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Extension E2 — layers are more than draw order
|
|
||||||
|
|
||||||
The sketch uses layers only to order `Render`. Layers are the natural unit for
|
|
||||||
three more things, all built on the existing `enable`/`disable` flag flips:
|
|
||||||
|
|
||||||
1. **Per-layer toggle.** `disable Hud` / `enable Hud` flips one flag; the layer's
|
|
||||||
handlers stop running and drawing. This is `disable Handler` generalized to a
|
|
||||||
named group — same one-flag-flip cost.
|
|
||||||
|
|
||||||
2. **Pause vs. tear-down.** A layer can keep drawing while its *update* handlers
|
|
||||||
are suspended:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
scene Overworld {
|
|
||||||
layer World { handler Move phase Update { … } handler Draw phase Render { … } }
|
|
||||||
layer Hud { handler DrawHud phase Render { … } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When a pause menu opens over the Overworld (see E3), `World`'s `Update`
|
|
||||||
handlers suspend but its `Render` handler still paints the frozen world behind
|
|
||||||
the menu. Today that requires a `if !paused` guard in every update handler;
|
|
||||||
with layers it's structural.
|
|
||||||
|
|
||||||
3. **Layer lifecycle hooks.** `on show` / `on hide` per layer, mirroring scene
|
|
||||||
`on enter`/`on exit`, for the toggle points. (Naming TBD — could fold into the
|
|
||||||
`@OnEnable`/`@OnDisable` annotations, which already exist for properties.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Extension E3 — the scene *stack* (the headline)
|
|
||||||
|
|
||||||
The sketch says "exactly one scene is active." That's the right default and the
|
|
||||||
wrong constraint. The states a mode register handles *worst* are the ones that
|
|
||||||
**overlay without replacing**: a pause menu over live gameplay, a dialog box, an
|
|
||||||
inventory screen, a confirmation prompt. With one register you either lose the
|
|
||||||
underlying state or hand-roll a "previous mode" variable and restore it.
|
|
||||||
|
|
||||||
Proposal: keep "one *base* scene," but allow scenes to be **pushed as overlays**.
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
scene Overworld {
|
|
||||||
layer World { handler Move phase Update { … } handler Draw phase Render { … } }
|
|
||||||
layer Hud { handler DrawHud phase Render { … } }
|
|
||||||
|
|
||||||
on enter { … }
|
|
||||||
handler PauseKey phase Input { if pressed(KEY_ESC) { push Pause } }
|
|
||||||
}
|
|
||||||
|
|
||||||
scene Pause overlay { # `overlay` = pushed, not swapped
|
|
||||||
on enter { dim_backdrop() }
|
|
||||||
layer Menu {
|
|
||||||
handler Nav phase Update {
|
|
||||||
if pressed(KEY_ESC) { pop } # back to Overworld, untouched
|
|
||||||
}
|
|
||||||
handler Draw phase Render { ui_render() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `push Name` runs `Name`'s `on enter` and makes it the top scene **without**
|
|
||||||
running the base scene's `on exit`. `pop` runs the overlay's `on exit` and
|
|
||||||
returns to whatever was beneath.
|
|
||||||
- **Update belongs to the top of the stack; render walks the whole stack bottom
|
|
||||||
to top.** So `Pause`'s `Menu` layer draws over `Overworld`'s frozen `World` and
|
|
||||||
`Hud`. This is the default that makes pause menus "just work." An overlay that
|
|
||||||
should let the layer beneath keep updating opts in with `push Name passthrough`.
|
|
||||||
- **The stack is a small fixed-capacity array of scene ids** (say 8) in a
|
|
||||||
compiler-owned buffer — not heap, not a linked structure. `push`/`pop` are an
|
|
||||||
index bump and an `on enter`/`on exit` call. Depth overflow is a compile-time
|
|
||||||
or trap decision (§9).
|
|
||||||
- `become` still exists and still means "swap the base scene" (full `on exit` →
|
|
||||||
`on enter`, stack cleared). `push`/`pop` are the overlay verbs. Keeping the two
|
|
||||||
distinct is what lets the common single-scene path stay a single register.
|
|
||||||
|
|
||||||
This is the extension that turns `scene` from "nicer mode register" into
|
|
||||||
something with no clean equivalent in the register world.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Extension E4 — scene-local state
|
|
||||||
|
|
||||||
A scene almost always has state that exists only while it's active — a battle's
|
|
||||||
turn counter, a menu's cursor index. Today that's a global register that other
|
|
||||||
scenes could stomp. Proposal: **`var` / `const` declared inside a `scene` is
|
|
||||||
scoped to it**, storage shared across scenes that are never simultaneously active
|
|
||||||
(the compiler can overlap their storage since only one base scene runs at a
|
|
||||||
time — an arena-per-scene, or a union).
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
scene Battle {
|
|
||||||
var turn = 0 # visible only inside Battle; reset by `on enter` if desired
|
|
||||||
layer World { handler Step phase Update { turn += 1 } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Reads/writes lower to a fixed offset in the scene's state block, no register
|
|
||||||
indirection.
|
|
||||||
- Overlay scenes (E3) that *can* be live simultaneously with their base cannot
|
|
||||||
share storage — the compiler keeps their blocks distinct. Base scenes that
|
|
||||||
never coexist share.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Extension E5 — parameterized transitions, and the reserved annotations
|
|
||||||
|
|
||||||
**Parameters on transitions.** `become`/`push` can carry arguments that the
|
|
||||||
target's `on enter` binds — so a battle knows which foes, a dialog knows which
|
|
||||||
line:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
scene Battle {
|
|
||||||
on enter (foe_kind: int, count: int) { for i in 0 .. count { spawn_foe(foe_kind) } }
|
|
||||||
}
|
|
||||||
# elsewhere:
|
|
||||||
become Battle(FOE_GOBLIN, 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
Lowers to argument stores into the scene's state block (E4) immediately before
|
|
||||||
the `on enter` call. No variadic runtime; the arity is checked at compile time.
|
|
||||||
|
|
||||||
**The already-reserved annotation form.** [LANGUAGE.md:374](LANGUAGE.md:374)
|
|
||||||
reserves `@OnEnter` / `@OnExit` as handler annotations "waiting on scene support."
|
|
||||||
This doc adopts them as the annotation spelling of `on enter` / `on exit`,
|
|
||||||
mirroring how `@OnStart` is the annotation form of `phase Start`:
|
|
||||||
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip
|
|
||||||
@OnEnter(Battle) handler Setup { … } # == Battle's `on enter`
|
|
||||||
@OnExit(Battle) handler Teardown { … }
|
|
||||||
```
|
|
||||||
|
|
||||||
Both spellings desugar to the same synthesized scene-lifecycle function; a scene
|
|
||||||
may use either, not both, for a given hook.
|
|
||||||
|
|
||||||
**`reads`/`writes` + scenes (forward-looking).** The `reads`/`writes` clauses are
|
|
||||||
parsed but unconsumed ([LANGUAGE.md:717](LANGUAGE.md:717)). Once an analysis pass
|
|
||||||
exists, a scene's layers declare which state they touch, and the scheduler can run
|
|
||||||
independent layers of the active scene in parallel within a phase — the scene
|
|
||||||
boundary gives the pass a natural scope to reason about. Noted as a destination,
|
|
||||||
not part of the first cut.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Lowering summary
|
|
||||||
|
|
||||||
Everything above reduces to existing runtime concepts:
|
|
||||||
|
|
||||||
| Construct | Lowers to |
|
|
||||||
|---|---|
|
|
||||||
| active base scene | one implicit register `__scene`, states numbered by decl order — literally a compiler-written `machine` |
|
|
||||||
| `become Name` | `on exit` call · `set __scene` · `on enter` call (two direct calls + store, as the sketch promises) |
|
|
||||||
| scene layers in a phase | the phase scheduler, after global handlers, dispatches on `__scene` to that scene's layer handlers in declaration order |
|
|
||||||
| `push`/`pop` (E3) | fixed-capacity scene-id array + index; render walks it, update reads its top |
|
|
||||||
| scene-owned entities (E1) | implicit kind tag at `spawn`; generated `despawn`-by-tag in synthesized `on exit`; reuses despawn hooks |
|
|
||||||
| layer toggle / pause (E2) | the same one-flag-flip as `disable Handler`, keyed per layer |
|
|
||||||
| scene-local `var` (E4) | fixed offsets in a per-scene state block; non-coexisting scenes share storage |
|
|
||||||
| transition args (E5) | arg stores into the state block before the `on enter` call |
|
|
||||||
| `@OnEnter`/`@OnExit` (E5) | the same synthesized lifecycle functions as `on enter`/`on exit` |
|
|
||||||
|
|
||||||
No heap, no dispatch tables, no new allocator. The active-scene path is a
|
|
||||||
register read and a static branch; the stack adds a small array only for programs
|
|
||||||
that push overlays.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Suggested implementation phases
|
|
||||||
|
|
||||||
Each is independently shippable and testable, matching how the repo phases work.
|
|
||||||
|
|
||||||
- **S0 — parse & lower the sketch.** ✅ **Done.** `scene`/`layer`/`on enter`/`on
|
|
||||||
exit`/`become` lowered to the implicit `machine`; the active scene is
|
|
||||||
snapshotted per phase so exactly one scene's layers dispatch in any phase.
|
|
||||||
[`examples/lang/scenes.ludic`](examples/lang/scenes.ludic) compiles, runs, and is checked
|
|
||||||
by `bin/x test`. This is the floor everything else builds on.
|
|
||||||
- **S1 — `@OnEnter`/`@OnExit` annotation form** (E5, cheap once S0 exists).
|
|
||||||
- **S2 — layer toggle & pause** (E2) on top of the existing `enable`/`disable`.
|
|
||||||
✅ *Toggle shipped* (via EVENTS-DESIGN EV1 layers): `enable layer L` / `disable
|
|
||||||
layer L` flips an `@LE_<L>` flag that gates the layer's handlers (emitted only
|
|
||||||
for toggled layers, so untouched scene programs stay byte-identical), and a
|
|
||||||
`public` layer fires `layer_<L>_show`/`_hide` — see
|
|
||||||
[`examples/events/layer_events.ludic`](examples/events/layer_events.ludic). Still open: the
|
|
||||||
*pause* half (keep drawing while `Update` handlers suspend) and `on show`/`on
|
|
||||||
hide` blocks.
|
|
||||||
- **S3 — the scene stack** (E3): `push`/`pop`/`overlay`/`passthrough`. The big one.
|
|
||||||
- **S4 — scene-owned entities** (E1) and **scene-local state** (E4).
|
|
||||||
- **S5 — transition parameters** (E5).
|
|
||||||
- **S6 (later) — `reads`/`writes` scheduling** (E5), gated on the analysis pass.
|
|
||||||
|
|
||||||
S0–S1 deliver the sketch as promised; S2–S3 are where the "great potential"
|
|
||||||
actually lands; S4–S5 are ergonomics; S6 is a performance destination.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Open decisions
|
|
||||||
|
|
||||||
1. **Re-entering a scene:** fresh entities/state, or resume? (Default proposed:
|
|
||||||
`become` = fresh, `push`/`pop` = the pushed scene is fresh each push, the base
|
|
||||||
underneath is untouched.)
|
|
||||||
2. **Stack depth:** compile-time cap with an error on overflow, or a runtime trap?
|
|
||||||
What capacity (8? configurable)?
|
|
||||||
3. **`passthrough` granularity:** does a passthrough overlay let *all* lower
|
|
||||||
layers update, or can it name which phases fall through?
|
|
||||||
4. **Layer hook naming:** `on show`/`on hide`, or reuse `@OnEnable`/`@OnDisable`?
|
|
||||||
5. **Scene-local storage sharing:** union non-coexisting scenes automatically, or
|
|
||||||
require an explicit opt-in so the sharing is visible in source?
|
|
||||||
6. **Global handlers and overlays:** do globals run once per frame regardless of
|
|
||||||
stack depth (proposed: yes), or per active scene?
|
|
||||||
7. **`become` from inside an overlay:** does it clear the stack (proposed: yes) or
|
|
||||||
is it an error while overlays are pushed?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Companion to [LANGUAGE.md §"Scenes & layers"](LANGUAGE.md) and the ordering
|
|
||||||
sketch in [`examples/lang/scenes.ludic`](examples/lang/scenes.ludic). Supersedes nothing
|
|
||||||
until the compiler work in §10 lands.*
|
|
||||||
|
|
@ -1,375 +0,0 @@
|
||||||
# Ludic Syntax Redesign — Cohesion Pass
|
|
||||||
|
|
||||||
A plan to make Ludic's syntax internally consistent. It fixes the drift between
|
|
||||||
the spec and the compiler, then unifies the grammar around two rules. Scope:
|
|
||||||
**full redesign (Phases 0–5)**. Named-field direction: **colon everywhere**.
|
|
||||||
|
|
||||||
> Status: **Phases 1–5 complete.** Every phase kept the compiler self-hosting to
|
|
||||||
> a fixpoint (`bin/x test` 14/14), and each syntax migration was proven
|
|
||||||
> behaviour-preserving (the migrated compiler compiles itself to byte-identical
|
|
||||||
> IR; every golden game renders byte-identically). Landed on branch
|
|
||||||
> `syntax-redesign-phase2` over a committed baseline on `main`.
|
|
||||||
>
|
|
||||||
> Coordinated with the toolchain agent (CLI front-end / `ludicc`+`ludic`
|
|
||||||
> binaries) via serialized reseeds of `selfhost/ludicc.seed.ll`; Phase 1 rode in
|
|
||||||
> alongside their `emit_*`/`main.ludic` work, combined suite **14/14 green**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why (the findings)
|
|
||||||
|
|
||||||
Verified against the self-hosted compiler ([selfhost/parse.ludic](selfhost/parse.ludic),
|
|
||||||
[selfhost/parse_game.ludic](selfhost/parse_game.ludic), [selfhost/lex.ludic](selfhost/lex.ludic)):
|
|
||||||
|
|
||||||
**Structural incoherence**
|
|
||||||
1. **Five micro-syntaxes for named parts** — `name: type = d` (fields), `name: type`
|
|
||||||
(params), `Field = { k = v }` (spawn), `[Name, {Tag}]` (query), whitespace
|
|
||||||
`phase X reads [..]` (system clauses), `key=value` (ui props).
|
|
||||||
2. **`=` means seven things, `:` means one** — assignment, default, record init,
|
|
||||||
ui prop, extern symbol, const value, `state X = N` all use `=`.
|
|
||||||
3. **No statement terminators** — `\n` and `;` both lex to `TK_NL`
|
|
||||||
([lex.ludic:45,121](selfhost/lex.ludic)) but the parser never requires a
|
|
||||||
separator, so `t.kind = k t.text = x t.ival = v` (three statements, spaces
|
|
||||||
only) is idiomatic.
|
|
||||||
|
|
||||||
**Broken / dead syntax (compiler-verified)**
|
|
||||||
4. `edge system` — **hard parse error** (documented at [LANGUAGE.md:188](LANGUAGE.md)). *(✅ fixed in Phase 1)*
|
|
||||||
5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented. *(✅ Phase 3d: now `@pure`)*
|
|
||||||
6. `@anno` + `reads/writes/needs/uses [..]` — parsed then thrown away
|
|
||||||
([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented. *(✅ Phase 3c: `needs`/`uses` dropped)*
|
|
||||||
7. `scene`/`layer`/`on enter` — full LANGUAGE.md section + [examples/lang/scenes.ludic](examples/lang/scenes.ludic),
|
|
||||||
**does not compile** (`expected declaration`).
|
|
||||||
8. `query (v) [..]` in a system signature — two LANGUAGE.md sections +
|
|
||||||
[examples/lang/qdecl.ludic](examples/lang/qdecl.ludic), **does not compile** (`parse error: {`). *(✅ implemented in Phase 1)*
|
|
||||||
9. `when cond {}` — documented ([LANGUAGE.md:329](LANGUAGE.md)) + in all three editor
|
|
||||||
highlighters, **never parsed**. *(✅ implemented in Phase 1 as an if-without-else alias)*
|
|
||||||
10. CLI `--emit-llvm`/`-o`/`--shared`/`--fmt` — documented, but `ludicc` only
|
|
||||||
accepts `--windowed`/`--headless` ([main.ludic:8-14](selfhost/main.ludic)).
|
|
||||||
|
|
||||||
**Philosophical splits**
|
|
||||||
11. Operators are words (`and`/`or`/`not`), symbols (`==`/`<=`), *and* functions
|
|
||||||
(`band`/`shl`) at once.
|
|
||||||
12. Three overlapping control families — `if`/`when`, `match`, `machine`/`become`
|
|
||||||
— and `enter` reuses `become`'s AST node ([parse.ludic:176-177](selfhost/parse.ludic)). *(Phase 4: `if`/`when` kept by choice; magic-int dispatch resolved)*
|
|
||||||
13. Typed components/structs exist, but real state lives in 64 untyped int
|
|
||||||
registers (`reg`/`set_reg`), so `machine`/`match` dispatch on magic numbers. *(✅ Phase 4: auto-numbered states + `enum` name the values)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The two rules everything converges on
|
|
||||||
|
|
||||||
**Rule A — `:` associates, `=` binds.**
|
|
||||||
- `:` introduces a *named part* and its type or value in a declarative structure:
|
|
||||||
component/struct fields' types, record initializers, ui props, (future) named
|
|
||||||
call arguments.
|
|
||||||
- `=` binds a value to a storage location or a constant: `let`, assignment
|
|
||||||
(`+=` …), `const` value, a field's **default**, and the extern symbol.
|
|
||||||
- A field declaration uses both, unambiguously: `x: int = 0` reads "`x` *has type*
|
|
||||||
`int` (`:`), *defaulting to* `0` (`=`)" — same shape as Rust/TypeScript.
|
|
||||||
- A record/spawn initializer is declarative, so it uses `:` — `Pos { x: 10 }`.
|
|
||||||
|
|
||||||
**Rule B — a statement ends at a newline (or `;` or `}`).**
|
|
||||||
- Newlines become significant. Two statements on one line require an explicit
|
|
||||||
`;`. `ludic-fmt` normalizes one statement per line and inserts/removes `;`.
|
|
||||||
|
|
||||||
Everything below is these two rules applied construct by construct.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Target grammar (before → after)
|
|
||||||
|
|
||||||
### Records / spawn initializers
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
# before
|
|
||||||
spawn Hero { Pos = { x = 10, y = 5 } Player = { } }
|
|
||||||
# after
|
|
||||||
spawn Hero {
|
|
||||||
Pos { x: 10, y: 5 }
|
|
||||||
Player {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`Field = { k = v }` → `Field { k: v }`. The component name is followed directly
|
|
||||||
by a record; fields use `:`. (Record literals elsewhere read the same:
|
|
||||||
`{ x: 10, y: 5 }`.)
|
|
||||||
|
|
||||||
### UI props → named-argument form
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
# before
|
|
||||||
panel id=Root w=288 pad=16 gap=6 align=center { label text="HI" size=26 }
|
|
||||||
# after
|
|
||||||
panel(id: Root, w: 288, pad: 16, gap: 6, align: center) {
|
|
||||||
label(text: "HI", size: 26)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
A widget becomes "a constructor with named args, then an optional child block."
|
|
||||||
This deletes the bespoke `key=value` dialect and reuses `:` + commas. (Lower-churn
|
|
||||||
alternative if the paren form is disliked: keep whitespace separation but colonize
|
|
||||||
— `panel id: Root w: 288` — still removes the `=` overload.)
|
|
||||||
|
|
||||||
### System clauses & modifiers → one annotation channel
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
# before
|
|
||||||
edge handler Move @deterministic reads [Vel] writes [Pos] phase FixedUpdate
|
|
||||||
query (p, v) [Pos, Vel] where a.x > 0 { … }
|
|
||||||
# after
|
|
||||||
@edge @deterministic
|
|
||||||
handler Move
|
|
||||||
phase FixedUpdate
|
|
||||||
reads [Vel] writes [Pos]
|
|
||||||
query (p, v) [Pos, Vel] where p.x > 0
|
|
||||||
{ … }
|
|
||||||
```
|
|
||||||
- Prefix modifier words (`edge`, `pure`, `export`) are **retired**; all modifiers
|
|
||||||
become `@annotations`, parsed into a real list on the node (not skipped). This
|
|
||||||
fixes the `edge system` parse bug (#4) by construction.
|
|
||||||
- `needs`/`uses` are dropped; `reads`/`writes` stay as the two structural clauses
|
|
||||||
and are **stored** (even if analysis is future work) rather than discarded.
|
|
||||||
- The `query (v) [..]` signature clause is **actually implemented** in
|
|
||||||
`parse_system` (#8), lowering to the same `S_QUERY` node as the inline `for`.
|
|
||||||
|
|
||||||
### extern
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # unchanged
|
|
||||||
```
|
|
||||||
The `= "symbol"` is a binding under Rule A — it stays.
|
|
||||||
|
|
||||||
### Statements
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
# before (legal today)
|
|
||||||
t.kind = kind t.text = text t.ival = ival
|
|
||||||
# after
|
|
||||||
t.kind = kind
|
|
||||||
t.text = text
|
|
||||||
t.ival = ival
|
|
||||||
# or, explicitly, on one line:
|
|
||||||
t.kind = kind; t.text = text; t.ival = ival
|
|
||||||
```
|
|
||||||
|
|
||||||
### Control flow (Phase 4)
|
|
||||||
- **`when` vs `if`** — `when` is now a working `if`-without-else alias (Phase 1).
|
|
||||||
Phase 4 decides whether to keep both spellings or collapse to one; if collapsed,
|
|
||||||
remove `when` from docs, the parser, and all editor highlighters together.
|
|
||||||
- **Typed states replace magic-int machines.** Introduce `enum`, and let
|
|
||||||
`machine` dispatch on a typed variable instead of a register:
|
|
||||||
```ludic
|
|
||||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
|
||||||
# before # after
|
|
||||||
const R_PHASE: int = 0 enum Phase { KnightMenu, KnightResolve, MageMenu, EnemyTurn }
|
|
||||||
machine R_PHASE { var phase: Phase = Phase.KnightMenu
|
|
||||||
state KnightMenu = 0 { … become … } machine phase {
|
|
||||||
state KnightResolve = 1 { … } state KnightMenu { … become KnightResolve }
|
|
||||||
} state KnightResolve { … }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`state X = N` loses the magic `= N` (ordinal comes from the enum). `become`
|
|
||||||
and `enter` (scenes) keep one shared lowering but read from a typed slot.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase sequence
|
|
||||||
|
|
||||||
Each phase is independently shippable and ends green on `bin/x test` +
|
|
||||||
`bin/x selfhost-test` (fixpoint).
|
|
||||||
|
|
||||||
### Phase 0 — Doctrine (done here)
|
|
||||||
Rules A and B above; colon-everywhere; `@`-annotations as the single modifier
|
|
||||||
channel; typed enums for state. No code.
|
|
||||||
|
|
||||||
### Phase 1 — Truth-in-documentation ✅ DONE
|
|
||||||
Made spec ⇄ compiler agree **before** any grammar change. What landed:
|
|
||||||
- ✅ **`edge system` crash fixed** (#4) — `parse_system` now consumes an optional
|
|
||||||
`edge` marker before `system` ([parse_game.ludic](selfhost/parse_game.ludic)).
|
|
||||||
(`edge` is a pure marker; the emitter never lowered it differently.)
|
|
||||||
- ✅ **Signature-`query` implemented** (#8) — `query (vars) [terms] where c` in a
|
|
||||||
system header desugars to the same `S_QUERY` node the inline `for` builds, so
|
|
||||||
`examples/lang/qdecl.ludic` compiles and runs. Also fixed multi-line clause parsing
|
|
||||||
(clauses may now span lines).
|
|
||||||
- ✅ **`when c { }` implemented** (#9) — as an `if`-without-else alias in
|
|
||||||
[parse.ludic](selfhost/parse.ludic). Docs + editors already listed it; now the
|
|
||||||
compiler agrees, so no editor-vocab churn was needed.
|
|
||||||
- ✅ **`scene`/`layer` marked not-yet-implemented** (#7) — prominent note in
|
|
||||||
LANGUAGE.md §"Scenes & layers" + a header on [examples/lang/scenes.ludic](examples/lang/scenes.ludic).
|
|
||||||
Full scene front-end + emission deferred (real work, out of Phase 1 scope).
|
|
||||||
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
|
|
||||||
updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now
|
|
||||||
listed there.
|
|
||||||
- ✅ **`bin/x test` guards drift** — added a `qsmoke qdecl` compile check. Suite
|
|
||||||
green (14/14 incl. the toolchain agent's CLI smoke tests).
|
|
||||||
- ✅ **CLI flags** (#10) — `--shared`/`--fmt`/wasm noted as dropped-with-the-C-driver
|
|
||||||
in LANGUAGE.md; `-o`/`--emit-llvm` were being re-added by the toolchain agent
|
|
||||||
(now real, verified in `bin/x test`); COMPILING.md updated by that agent.
|
|
||||||
- Deferred (intentionally): `pure`-is-ignored (#5) is undocumented and harmless;
|
|
||||||
it will be folded into `@pure` in Phase 3 rather than churned now.
|
|
||||||
- **Not done / by design:** `scenes.ludic` is *not* added to `bin/x test` (it can't
|
|
||||||
compile yet — a positive test would fail; the header note + LANGUAGE.md warning
|
|
||||||
cover the drift instead).
|
|
||||||
|
|
||||||
### Phase 2 — Statement separation (Rule B) ✅ DONE
|
|
||||||
Landed on branch `syntax-redesign-phase2` (baseline committed on `main` first).
|
|
||||||
- ✅ **Parser enforces a separator** — `block()` requires a newline or `;` after
|
|
||||||
each statement, else `expected newline or ';' between statements`
|
|
||||||
([parse.ludic](selfhost/parse.ludic)). Also fixed `if`-without-`else` swallowing
|
|
||||||
its trailing separator (it now peeks for `else` and restores if absent).
|
|
||||||
- ✅ **Interpretation chosen:** *require a separator*, not *reflow to one-per-line*.
|
|
||||||
The migration **inserts `;` at statement boundaries** and leaves lines intact —
|
|
||||||
comment-safe, minimal-diff, and it makes boundaries visible without an
|
|
||||||
opinionated reflow. One-per-line stays the recommended hand-written form.
|
|
||||||
- ✅ **Migration tool** ([tools/ludic-tools/migrate_separators.c](tools/ludic-tools/migrate_separators.c),
|
|
||||||
reuses the toolchain lexer) with a
|
|
||||||
**verification oracle**: a `;` inserted at a real boundary is a semantic no-op,
|
|
||||||
proven by the migrated compiler compiling itself to **IR byte-identical to the
|
|
||||||
seed** and every golden game rendering identically. ~1100 boundaries across the
|
|
||||||
corpus (examples, runtime, and the 25 self-host fragments).
|
|
||||||
- ✅ **Reseeded** to the strict compiler (19557 lines); C-free bootstrap fixpoint
|
|
||||||
holds; `bin/x test` 14/14; all goldens byte-identical; qdecl runs correctly.
|
|
||||||
- ✅ **Docs updated** — Rule B documented in LANGUAGE.md §Statements; BOOTSTRAP.md
|
|
||||||
R1 (which advertised no-separator juxtaposition as legal) and its stale code
|
|
||||||
fences updated; `check-docs` (now a live strict parse gate) green across all docs.
|
|
||||||
|
|
||||||
**Bug found & fixed en route:** a multi-line string literal in
|
|
||||||
[emit_expr.ludic](selfhost/emit_expr.ludic) (`emit(")<newline>")`) lexed fine in
|
|
||||||
the self-host lexer but the **C toolchain lexer** (`ludic_syntax.h`, shared by
|
|
||||||
sepfix, `ludic-fmt`, and the LSP) stops strings at newline — so it mis-lexed and
|
|
||||||
`ludic-fmt` would corrupt such a file. Converted it to the byte-identical `\n`
|
|
||||||
escape. **Open follow-up:** align the C lexer to allow newlines in strings, or
|
|
||||||
forbid literal newlines in string literals language-wide (the two lexers should
|
|
||||||
agree). Flagged to the toolchain owners.
|
|
||||||
|
|
||||||
### Phase 3 — Named-field unification (Rule A)
|
|
||||||
|
|
||||||
**3a — spawn/record initializers ✅ DONE.** `Comp = { f = v }` → `Comp { f: v }`.
|
|
||||||
`record()` requires `:` and `parse_spawn()` drops the `=` before the record
|
|
||||||
([parse.ludic](selfhost/parse.ludic), [parse_game.ludic](selfhost/parse_game.ludic)).
|
|
||||||
The `=` is now assignment/const/default/extern-binding only. Migration tool:
|
|
||||||
[migrate_records.c](tools/ludic-tools/migrate_records.c) (spawn-context aware).
|
|
||||||
Records live only in games, so the seed was unaffected; verified every golden
|
|
||||||
byte-identical, old `=` form now rejected, reseeded, `bin/x test` 14/14. Doc examples
|
|
||||||
updated (LANGUAGE.md, BOOTSTRAP.md R2).
|
|
||||||
|
|
||||||
**3b — ui props → `key: value` ✅ DONE.** `panel id=Root w=288` → `panel id: Root
|
|
||||||
w: 288`. `parse_widget` now reads props with `:` ([parse_game.ludic](selfhost/parse_game.ludic)).
|
|
||||||
Chose the **colonized** form over parenthesized named-args: it satisfies Rule A
|
|
||||||
(the `=` overload is gone) with minimal churn, needs no new grammar, and `emit_ui`
|
|
||||||
(which reads the AST) and `ludic-fmt` (which formats `:` correctly by default)
|
|
||||||
were both untouched. Migration: [migrate_ui.c](tools/ludic-tools/migrate_ui.c).
|
|
||||||
menu golden byte-identical, old `=` form rejected, reseeded, `bin/x test` 14/14.
|
|
||||||
(The parenthesized form `panel(id: Root, w: 288)` remains a possible future
|
|
||||||
refinement if the language ever gains named call arguments.)
|
|
||||||
|
|
||||||
**3c — dropped the dead `needs`/`uses` clause synonyms ✅ DONE.** `reads`/`writes`
|
|
||||||
stay (documented; still parsed-and-reserved). `needs`/`uses` were undocumented and
|
|
||||||
unused anywhere in the corpus — removed from `parse_system`. *Not done:* actually
|
|
||||||
*storing* reads/writes on the node for an analysis pass — that's analysis
|
|
||||||
infrastructure, out of scope for a syntax pass.
|
|
||||||
|
|
||||||
**3d — modifiers → `@`-annotations ✅ DONE.** `edge`/`pure`/`export` prefix keywords
|
|
||||||
are retired; declaration modifiers are now leading `@annotations`: `@export fn`,
|
|
||||||
`@edge system`, `@pure`, `@deterministic`. `parse_one_decl` collects a leading
|
|
||||||
`@anno` run and `@export` sets the fn export flag ([parse.ludic](selfhost/parse.ludic));
|
|
||||||
the dead `edge`-dispatch was removed from `parse_system`. Migrated the one
|
|
||||||
`@export` user ([examples/library/combat.ludic](examples/library/combat.ludic)); old
|
|
||||||
prefix forms now rejected. Behavior-identical: the export flag is parse-only in
|
|
||||||
the self-hosted emitter (it emits `@fn_<name>` for every function and never reads
|
|
||||||
the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so
|
|
||||||
`@export` and the old `export` produce byte-identical IR. Reseeded, fixpoint
|
|
||||||
holds, `bin/x test` 14/14, goldens byte-identical.
|
|
||||||
|
|
||||||
**Phase 3 is complete.** The `=`/`:` overload (finding #2) and the modifier-zoo
|
|
||||||
(findings #5, #6) are resolved; `:` associates and `=` binds throughout.
|
|
||||||
|
|
||||||
Each sub-phase follows the proven pattern: parser change → verification-gated
|
|
||||||
migration (IR byte-identical / goldens identical) → reseed → docs. The migration
|
|
||||||
tools ([migrate_separators.c](tools/ludic-tools/migrate_separators.c),
|
|
||||||
[migrate_records.c](tools/ludic-tools/migrate_records.c)) are the reusable spine.
|
|
||||||
|
|
||||||
### Phase 4 — Control-flow & state consolidation
|
|
||||||
|
|
||||||
**4a — machine states auto-number ✅ DONE.** `state KnightMenu = 0 { }` →
|
|
||||||
`state KnightMenu { }`; a state's value is its declaration index (an explicit
|
|
||||||
`= expr` still works). Removes the magic constants from state machines
|
|
||||||
([parse.ludic](selfhost/parse.ludic)). combat.ludic migrated; chronorift golden
|
|
||||||
byte-identical.
|
|
||||||
|
|
||||||
**4b — `enum` types ✅ DONE.** `enum Action { Attack, Guard, Item, Flee }` declares
|
|
||||||
named `int` constants; a variant is a compile-time int accessed as `Action.Guard`
|
|
||||||
(= 1), numbered by order. Parser `parse_enum` + dispatch, `enum_ordinal` resolver
|
|
||||||
in [emit_core.ludic](selfhost/emit_core.ludic), and `Enum.Variant` handling in
|
|
||||||
[emit_expr.ludic](selfhost/emit_expr.ludic). combat.ludic's battle menus now
|
|
||||||
dispatch on `KnightAct`/`MageAct` instead of `0..3`; chronorift golden
|
|
||||||
byte-identical. Editor vocab (`ludic_syntax.h`, JetBrains, TextMate, emacs) gained
|
|
||||||
`enum` and lost the retired `edge`/`export`/`pure` decl keywords; check-vocabulary
|
|
||||||
+ test-tools green. **Scoped:** enums are a naming layer over `int` (no distinct
|
|
||||||
runtime type / enum-typed variables yet) — that keeps register/save semantics
|
|
||||||
untouched, which the "enum var replaces the register" vision would have to solve.
|
|
||||||
|
|
||||||
**`when` vs `if` — kept both (decision).** `when` stays as the `if`-without-else
|
|
||||||
spelling: it is not incoherent so much as a readability signal ("no else here"),
|
|
||||||
it is documented and highlighted, and it is a pure alias with no semantic overlap
|
|
||||||
to untangle. The real target of finding #12 — dispatch on magic integers — is
|
|
||||||
addressed by 4a/4b, not by collapsing `if`/`when`.
|
|
||||||
|
|
||||||
**Bitwise operators — kept as functions (decision).** `band`/`bor`/`bxor`/`bnot`/
|
|
||||||
`shl`/`shr` stay functions, documented as the deliberate "one spelling, symbols
|
|
||||||
stay free" choice (LANGUAGE.md §Expressions already states this). Promoting them
|
|
||||||
to operators would re-introduce the symbol soup the current design avoids.
|
|
||||||
|
|
||||||
### Phase 5 — Vocabulary anchored to the compiler ✅ DONE
|
|
||||||
The editor vocabulary already stayed in sync *with itself* (`check-vocabulary.py`
|
|
||||||
compares `ludic_syntax.h`, the JetBrains lexer, and the TextMate grammar). The
|
|
||||||
missing anchor was the **compiler**: a keyword could be highlighted everywhere
|
|
||||||
and still be silently unparsed. Closed both loops:
|
|
||||||
- ✅ **Vocabulary ⇄ parser.** `check-vocabulary.py` now extracts every keyword
|
|
||||||
`selfhost/parse*.ludic` dispatches on (`is_id(...)` / `streq(t.text, ...)`) and
|
|
||||||
requires the header's declaration + clause keywords to be a subset — with a
|
|
||||||
`LUDIC_KW_RESERVED` escape hatch for documented, not-yet-implemented keywords
|
|
||||||
(`scene`/`layer`/`on`/`start`), itself checked so a reserved word that gets
|
|
||||||
implemented must be promoted. Verified it catches an injected bogus keyword.
|
|
||||||
- ✅ **Reconciled the drift it exposed.** Removed the highlighted-but-unparsed
|
|
||||||
`scene`/`layer`/`on`/`start` (→ RESERVED) and the never-implemented
|
|
||||||
`needs`/`uses`/`requires`/`ensures`/`invariant`/`effects` clause words, and the
|
|
||||||
retired `edge`/`export`/`pure` prefix modifiers, from `ludic_syntax.h`, the
|
|
||||||
JetBrains lexer, the TextMate grammar, and the emacs mode; added `enum`/`main`.
|
|
||||||
`@`-annotations already highlight generically (`@[A-Za-z_]…`). test-tools 28/0.
|
|
||||||
- ✅ **Doc-fence compilation** — the other half of "single source of truth" — was
|
|
||||||
already live: `check-docs.py` compiles every ` ```ludic ` fence through the
|
|
||||||
self-hosted `ludicc --fmt` parse gate (revived during Phase 1's coordination).
|
|
||||||
|
|
||||||
Full generation-from-one-list (emit the editor files from a manifest) was not
|
|
||||||
needed: the bidirectional *checks* give the same guarantee — nothing can drift
|
|
||||||
without CI failing — without a code-generation step to maintain.
|
|
||||||
|
|
||||||
**Phases 1–5 are complete.**
|
|
||||||
|
|
||||||
### Phase 6 — vocabulary rename + annotation DSL ✅ DONE (follow-on request)
|
|
||||||
Renamed the core nouns: `game`/`module` → `program`, `main` → `entry`,
|
|
||||||
`component` → `property`, `archetype` → `model`, `system` → `handler`. Done via a
|
|
||||||
transitional self-hosting bootstrap (accept both → reseed → move the compiler's
|
|
||||||
own source to new keywords + tighten → reseed); old keywords now rejected.
|
|
||||||
Token-safe corpus migration ([rename_kw.c](tools/ludic-tools/rename_kw.c)), goldens
|
|
||||||
byte-identical. Reconciled the LSP indexer, editor vocab, check-docs wrapper, and
|
|
||||||
docs; fixed two pre-existing toolchain bugs (a `set -e` bug in build-tools.sh that
|
|
||||||
blocked all editor-binary rebuilds, and a stale LSP test offset).
|
|
||||||
|
|
||||||
Added an **annotation DSL**: `@Queries(these: [Prop{constraint}, …], on: Model)` on
|
|
||||||
a handler desugars to the existing `S_QUERY` loop (each property binds by its own
|
|
||||||
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
|
|
||||||
tag), and `@Handles(…)` on a program parses as documentation. See
|
|
||||||
[examples/lang/annotations.ludic](examples/lang/annotations.ludic); bin/x test 15/15. All thirteen findings are resolved or resolved by an
|
|
||||||
explicit, documented decision.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Decision log
|
|
||||||
- **Scope:** full redesign, Phases 0–5. *(chosen)*
|
|
||||||
- **Named fields:** colon everywhere; `=` is binding-only. *(chosen)*
|
|
||||||
- **Open — Phase 4 detail:** typed `enum` state vs. keep integer registers.
|
|
||||||
Recommended: typed enums (fixes #13), but it's the deepest change; can be
|
|
||||||
deferred without blocking Phases 1–3.
|
|
||||||
- **Open — ui props:** paren named-args (`panel(id: Root)`) vs. colonized
|
|
||||||
whitespace (`panel id: Root`). Recommended: paren form for full cohesion.
|
|
||||||
- **Open — bitwise ops:** functions (status quo, documented) vs. operators.
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.1.0
|
0.22.0
|
||||||
|
|
|
||||||
1981
assets/kenney/tiny-dungeon/Tiled/sampleMap.tmj
Normal file
1981
assets/kenney/tiny-dungeon/Tiled/sampleMap.tmj
Normal file
File diff suppressed because it is too large
Load diff
3
assets/polyhaven/LICENSE.txt
Normal file
3
assets/polyhaven/LICENSE.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Everything under assets/polyhaven/ is fetched from https://polyhaven.com and is
|
||||||
|
released by Poly Haven under CC0 1.0 (public domain). Files are not committed;
|
||||||
|
see manifest.txt for the exact sources. Re-fetch with tools/glgen/fetch_assets.sh.
|
||||||
29
assets/tiled-fixtures/README.md
Normal file
29
assets/tiled-fixtures/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Tiled golden fixtures
|
||||||
|
|
||||||
|
The curated, version-pinned corpus for the Ludic Tiled reader (design record:
|
||||||
|
[Design: Tiled maps](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design%2FTiled),
|
||||||
|
issues #67–#74). No single Tiled file covers the format surface, so this is a
|
||||||
|
subset of the official [`mapeditor/tiled`](https://github.com/mapeditor/tiled)
|
||||||
|
`examples/` tree plus a few hand-authored files for the gaps the official
|
||||||
|
examples miss.
|
||||||
|
|
||||||
|
## Vendored from mapeditor/tiled (`examples/`)
|
||||||
|
|
||||||
|
Fetched from `https://raw.githubusercontent.com/mapeditor/tiled/master/examples/`.
|
||||||
|
Tiled's example assets carry their own licenses (see the upstream repo's
|
||||||
|
per-folder `*.license`/README); vendored here with attribution for testing only.
|
||||||
|
|
||||||
|
| File | Exercises |
|
||||||
|
|---|---|
|
||||||
|
| `desert.tmx` + `desert.tsx` | orthogonal, external `.tsx`, base64+zlib (P0/P1) |
|
||||||
|
| `sewers.tmx` | orthogonal, base64+zlib, embedded tileset, opacity (P0) |
|
||||||
|
| `orthogonal-outside.tmx` | object layers, shapes, custom properties (P4) |
|
||||||
|
| `perspective_walls.tsx` | per-tile bool properties, `<tileoffset>` (P4) |
|
||||||
|
| `isometric_grass_and_water.tmx` | isometric orientation, Wang set (P5) |
|
||||||
|
| `hexagonal-mini.tmx` | hexagonal orientation (P5) |
|
||||||
|
|
||||||
|
## Hand-authored (CC0 / public domain)
|
||||||
|
|
||||||
|
| File | Exercises |
|
||||||
|
|---|---|
|
||||||
|
| `handmade.tmx` + `handmade.tsx` | a 4×4 orthogonal map whose four layers carry the **same** GID array in CSV, base64-uncompressed, base64+gzip and base64+zlib — so every encoding path must decode identically. The last tile is GID 1 with the horizontal-flip flag (`0x80000001`), forcing real GID decode. `handmade.tsx` also carries a `solid` bool property and a per-tile `<objectgroup>` collision shape for P2. |
|
||||||
16
assets/tiled-fixtures/anim_map.tmx
Normal file
16
assets/tiled-fixtures/anim_map.tmx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P3 fixture: animated tile + tile object (issue #71). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="2">
|
||||||
|
<tileset firstgid="1" source="anim_tiles.tsx"/>
|
||||||
|
<layer id="1" name="bg" width="4" height="4">
|
||||||
|
<data encoding="csv">
|
||||||
|
1,0,0,0,
|
||||||
|
0,0,0,0,
|
||||||
|
0,0,0,0,
|
||||||
|
0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="2" name="objects">
|
||||||
|
<object id="1" gid="53" x="16" y="32" width="16" height="16"/>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
12
assets/tiled-fixtures/anim_tiles.tsx
Normal file
12
assets/tiled-fixtures/anim_tiles.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored animated tileset over the Kenney atlas (issue #71). CC0. -->
|
||||||
|
<tileset version="1.10" tiledversion="1.10.2" name="anim" tilewidth="16" tileheight="16" spacing="1" tilecount="132" columns="12">
|
||||||
|
<image source="../kenney/tiny-dungeon/Tilemap/tilemap.png" width="203" height="186"/>
|
||||||
|
<tile id="0">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="0" duration="100"/>
|
||||||
|
<frame tileid="40" duration="100"/>
|
||||||
|
<frame tileid="80" duration="100"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
267
assets/tiled-fixtures/beach_tileset.tsx
Normal file
267
assets/tiled-fixtures/beach_tileset.tsx
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.8" tiledversion="1.8.2" name="beach_tileset" tilewidth="16" tileheight="16" tilecount="936" columns="36">
|
||||||
|
<image source="beach_tileset.png" width="576" height="416"/>
|
||||||
|
<tile id="37">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="37" duration="250"/>
|
||||||
|
<frame tileid="46" duration="250"/>
|
||||||
|
<frame tileid="55" duration="250"/>
|
||||||
|
<frame tileid="64" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="38">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="38" duration="250"/>
|
||||||
|
<frame tileid="47" duration="250"/>
|
||||||
|
<frame tileid="56" duration="250"/>
|
||||||
|
<frame tileid="65" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="39">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="39" duration="250"/>
|
||||||
|
<frame tileid="48" duration="250"/>
|
||||||
|
<frame tileid="57" duration="250"/>
|
||||||
|
<frame tileid="66" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="41">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="41" duration="250"/>
|
||||||
|
<frame tileid="50" duration="250"/>
|
||||||
|
<frame tileid="59" duration="250"/>
|
||||||
|
<frame tileid="68" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="42">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="42" duration="250"/>
|
||||||
|
<frame tileid="51" duration="250"/>
|
||||||
|
<frame tileid="60" duration="250"/>
|
||||||
|
<frame tileid="69" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="43">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="43" duration="250"/>
|
||||||
|
<frame tileid="52" duration="250"/>
|
||||||
|
<frame tileid="61" duration="250"/>
|
||||||
|
<frame tileid="70" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="73">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="73" duration="250"/>
|
||||||
|
<frame tileid="82" duration="250"/>
|
||||||
|
<frame tileid="91" duration="250"/>
|
||||||
|
<frame tileid="100" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="75">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="75" duration="250"/>
|
||||||
|
<frame tileid="84" duration="250"/>
|
||||||
|
<frame tileid="93" duration="250"/>
|
||||||
|
<frame tileid="102" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="76">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="76" duration="250"/>
|
||||||
|
<frame tileid="85" duration="250"/>
|
||||||
|
<frame tileid="94" duration="250"/>
|
||||||
|
<frame tileid="103" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="77">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="77" duration="250"/>
|
||||||
|
<frame tileid="86" duration="250"/>
|
||||||
|
<frame tileid="95" duration="250"/>
|
||||||
|
<frame tileid="104" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="79">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="79" duration="250"/>
|
||||||
|
<frame tileid="88" duration="250"/>
|
||||||
|
<frame tileid="97" duration="250"/>
|
||||||
|
<frame tileid="106" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="109">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="109" duration="250"/>
|
||||||
|
<frame tileid="118" duration="250"/>
|
||||||
|
<frame tileid="127" duration="250"/>
|
||||||
|
<frame tileid="136" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="110">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="110" duration="250"/>
|
||||||
|
<frame tileid="119" duration="250"/>
|
||||||
|
<frame tileid="128" duration="250"/>
|
||||||
|
<frame tileid="137" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="114">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="114" duration="250"/>
|
||||||
|
<frame tileid="123" duration="250"/>
|
||||||
|
<frame tileid="132" duration="250"/>
|
||||||
|
<frame tileid="141" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="115">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="115" duration="250"/>
|
||||||
|
<frame tileid="124" duration="250"/>
|
||||||
|
<frame tileid="133" duration="250"/>
|
||||||
|
<frame tileid="142" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="146">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="146" duration="250"/>
|
||||||
|
<frame tileid="155" duration="250"/>
|
||||||
|
<frame tileid="164" duration="250"/>
|
||||||
|
<frame tileid="173" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="148">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="148" duration="250"/>
|
||||||
|
<frame tileid="157" duration="250"/>
|
||||||
|
<frame tileid="166" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="150">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="150" duration="250"/>
|
||||||
|
<frame tileid="159" duration="250"/>
|
||||||
|
<frame tileid="168" duration="250"/>
|
||||||
|
<frame tileid="177" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="181">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="181" duration="250"/>
|
||||||
|
<frame tileid="190" duration="250"/>
|
||||||
|
<frame tileid="199" duration="250"/>
|
||||||
|
<frame tileid="208" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="182">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="182" duration="250"/>
|
||||||
|
<frame tileid="191" duration="250"/>
|
||||||
|
<frame tileid="200" duration="250"/>
|
||||||
|
<frame tileid="209" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="186">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="186" duration="250"/>
|
||||||
|
<frame tileid="195" duration="250"/>
|
||||||
|
<frame tileid="204" duration="250"/>
|
||||||
|
<frame tileid="213" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="187">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="187" duration="250"/>
|
||||||
|
<frame tileid="196" duration="250"/>
|
||||||
|
<frame tileid="205" duration="250"/>
|
||||||
|
<frame tileid="214" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="217">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="217" duration="250"/>
|
||||||
|
<frame tileid="226" duration="250"/>
|
||||||
|
<frame tileid="235" duration="250"/>
|
||||||
|
<frame tileid="244" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="219">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="219" duration="250"/>
|
||||||
|
<frame tileid="228" duration="250"/>
|
||||||
|
<frame tileid="237" duration="250"/>
|
||||||
|
<frame tileid="246" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="220">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="220" duration="250"/>
|
||||||
|
<frame tileid="229" duration="250"/>
|
||||||
|
<frame tileid="238" duration="250"/>
|
||||||
|
<frame tileid="247" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="221">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="221" duration="250"/>
|
||||||
|
<frame tileid="230" duration="250"/>
|
||||||
|
<frame tileid="239" duration="250"/>
|
||||||
|
<frame tileid="248" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="223">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="223" duration="250"/>
|
||||||
|
<frame tileid="232" duration="250"/>
|
||||||
|
<frame tileid="241" duration="250"/>
|
||||||
|
<frame tileid="250" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="253">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="253" duration="250"/>
|
||||||
|
<frame tileid="262" duration="250"/>
|
||||||
|
<frame tileid="271" duration="250"/>
|
||||||
|
<frame tileid="280" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="254">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="254" duration="250"/>
|
||||||
|
<frame tileid="263" duration="250"/>
|
||||||
|
<frame tileid="272" duration="250"/>
|
||||||
|
<frame tileid="281" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="255">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="255" duration="250"/>
|
||||||
|
<frame tileid="264" duration="250"/>
|
||||||
|
<frame tileid="273" duration="250"/>
|
||||||
|
<frame tileid="282" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="257">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="257" duration="250"/>
|
||||||
|
<frame tileid="266" duration="250"/>
|
||||||
|
<frame tileid="275" duration="250"/>
|
||||||
|
<frame tileid="284" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="258">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="258" duration="250"/>
|
||||||
|
<frame tileid="267" duration="250"/>
|
||||||
|
<frame tileid="276" duration="250"/>
|
||||||
|
<frame tileid="285" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
<tile id="259">
|
||||||
|
<animation>
|
||||||
|
<frame tileid="259" duration="250"/>
|
||||||
|
<frame tileid="268" duration="250"/>
|
||||||
|
<frame tileid="277" duration="250"/>
|
||||||
|
<frame tileid="286" duration="250"/>
|
||||||
|
</animation>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
9
assets/tiled-fixtures/collision.tsx
Normal file
9
assets/tiled-fixtures/collision.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored collision tileset for the Ludic Tiled demos (issue #69). CC0. -->
|
||||||
|
<tileset version="1.10" tiledversion="1.10.2" name="collision" tilewidth="16" tileheight="16" tilecount="2" columns="1">
|
||||||
|
<tile id="1">
|
||||||
|
<properties>
|
||||||
|
<property name="oneway" type="bool" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
1
assets/tiled-fixtures/demo.world
Normal file
1
assets/tiled-fixtures/demo.world
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"maps": [{"fileName": "zstd_map.tmx", "x": 0, "y": 0, "width": 384, "height": 256}, {"fileName": "grid_maze.tmx", "x": 384, "y": 0, "width": 128, "height": 80}], "onlyShowAdjacentMaps": false, "type": "world"}
|
||||||
9
assets/tiled-fixtures/desert.tmx
Normal file
9
assets/tiled-fixtures/desert.tmx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.0" tiledversion="1.1.5" orientation="orthogonal" renderorder="right-down" width="40" height="40" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="desert.tsx"/>
|
||||||
|
<layer id="1" name="Ground" width="40" height="40">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJztmNkKwjAQRaN9cAPrAq5Yq3Xf6v9/nSM2VIbQJjEZR+nDwQZScrwztoORECLySBcIgZ7nc2y4KfyWDLx+Jb9nViNgDEwY+KioAXUgQN4+zpoCMwPmQAtoAx2CLFbA2oDEo9+hwG8DnIDtF/2K8ks086Tw2zH0uyMv7HcRr/6/EvvhnsPrsrxwX7rwU/0ODig/eV3mh3N1ld8eraWPaX6+64s9McesfrqcHfg1MpoifxcVEWjukyw+9AtFPl/I71pER3Of6j4bv7HI54s+MChhqLlPdZ/P3qMmFuo5h5NnTOhjM5tReN2yT51n5/v7J3F0vi46fk+ne7aX0i9l6If7mpufTX3f5wsqv9TAD2fJLT9VrTn7UeZnM5tR+v0LMQOHXwFnxe2/warGFRWf8QDjOLfP
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
68
assets/tiled-fixtures/desert.tsx
Normal file
68
assets/tiled-fixtures/desert.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.4" tiledversion="1.4.3" name="Desert" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
|
||||||
|
<image source="tmw_desert_spacing.png" width="265" height="199"/>
|
||||||
|
<tile id="30" probability="0.01"/>
|
||||||
|
<tile id="31" probability="0.01"/>
|
||||||
|
<tile id="37" probability="0.01"/>
|
||||||
|
<tile id="38" probability="0.01"/>
|
||||||
|
<tile id="39" probability="0.01"/>
|
||||||
|
<tile id="45" probability="0"/>
|
||||||
|
<tile id="46" probability="0.01"/>
|
||||||
|
<tile id="47" probability="0.01"/>
|
||||||
|
<wangsets>
|
||||||
|
<wangset name="Desert" type="corner" tile="5">
|
||||||
|
<wangcolor name="Desert" color="#ff0000" tile="29" probability="1"/>
|
||||||
|
<wangcolor name="Brick" color="#00ff00" tile="9" probability="1"/>
|
||||||
|
<wangcolor name="Cobblestone" color="#0000ff" tile="33" probability="1"/>
|
||||||
|
<wangcolor name="Dirt" color="#ff7700" tile="14" probability="1"/>
|
||||||
|
<wangtile tileid="0" wangid="0,1,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="1" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="2" wangid="0,1,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="3" wangid="0,4,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="4" wangid="0,4,0,4,0,1,0,4"/>
|
||||||
|
<wangtile tileid="5" wangid="0,1,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="6" wangid="0,1,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="7" wangid="0,1,0,1,0,4,0,1"/>
|
||||||
|
<wangtile tileid="8" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="9" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="10" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="11" wangid="0,1,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="12" wangid="0,4,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="13" wangid="0,4,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="14" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="15" wangid="0,1,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="16" wangid="0,2,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="17" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="18" wangid="0,1,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="19" wangid="0,2,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="20" wangid="0,2,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="21" wangid="0,4,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="22" wangid="0,4,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="23" wangid="0,1,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="24" wangid="0,1,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="25" wangid="0,1,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="26" wangid="0,1,0,1,0,3,0,1"/>
|
||||||
|
<wangtile tileid="27" wangid="0,1,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="28" wangid="0,2,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="29" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="30" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="31" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="32" wangid="0,3,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="33" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="34" wangid="0,1,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="35" wangid="0,3,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="36" wangid="0,3,0,3,0,1,0,3"/>
|
||||||
|
<wangtile tileid="37" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="38" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="39" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="40" wangid="0,3,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="41" wangid="0,3,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="42" wangid="0,1,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="43" wangid="0,1,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="44" wangid="0,3,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="45" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="46" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="47" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
</wangset>
|
||||||
|
</wangsets>
|
||||||
|
</tileset>
|
||||||
14
assets/tiled-fixtures/grid_maze.tmx
Normal file
14
assets/tiled-fixtures/grid_maze.tmx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored map for the Ludic Tiled demos (issue #69). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="8" height="5" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="collision.tsx"/>
|
||||||
|
<layer id="1" name="collision" width="8" height="5">
|
||||||
|
<data encoding="csv">
|
||||||
|
1,1,1,1,1,1,1,1,
|
||||||
|
1,0,0,0,0,0,0,1,
|
||||||
|
1,0,1,1,1,1,0,1,
|
||||||
|
1,0,0,0,0,1,0,1,
|
||||||
|
1,1,1,1,1,1,1,1
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
65
assets/tiled-fixtures/handmade.tmj
Normal file
65
assets/tiled-fixtures/handmade.tmj
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"type": "map",
|
||||||
|
"version": "1.10",
|
||||||
|
"tiledversion": "1.10.2",
|
||||||
|
"orientation": "orthogonal",
|
||||||
|
"renderorder": "right-down",
|
||||||
|
"width": 4,
|
||||||
|
"height": 4,
|
||||||
|
"tilewidth": 16,
|
||||||
|
"tileheight": 16,
|
||||||
|
"infinite": false,
|
||||||
|
"nextlayerid": 3,
|
||||||
|
"nextobjectid": 1,
|
||||||
|
"tilesets": [
|
||||||
|
{
|
||||||
|
"firstgid": 1,
|
||||||
|
"source": "handmade.tsx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"type": "tilelayer",
|
||||||
|
"id": 1,
|
||||||
|
"name": "csv",
|
||||||
|
"width": 4,
|
||||||
|
"height": 4,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"opacity": 1,
|
||||||
|
"visible": true,
|
||||||
|
"data": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
2147483649
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tilelayer",
|
||||||
|
"id": 2,
|
||||||
|
"name": "zlib",
|
||||||
|
"width": 4,
|
||||||
|
"height": 4,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"opacity": 1,
|
||||||
|
"visible": true,
|
||||||
|
"encoding": "base64",
|
||||||
|
"compression": "zlib",
|
||||||
|
"data": "eJwNw4cNACAMBLEPvYaVGT1nySYpMbOwsrFzcHJx8/DS+WjSDw1EAPo="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
assets/tiled-fixtures/handmade.tmx
Normal file
25
assets/tiled-fixtures/handmade.tmx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored fixture for the Ludic Tiled reader (issue #67). Public domain (CC0). -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="handmade.tsx"/>
|
||||||
|
<layer id="1" name="csv" width="4" height="4">
|
||||||
|
<data encoding="csv">
|
||||||
|
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,2147483649
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="base64" width="4" height="4">
|
||||||
|
<data encoding="base64">
|
||||||
|
AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAAQAAgA==
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="3" name="gzip" width="4" height="4">
|
||||||
|
<data encoding="base64" compression="gzip">
|
||||||
|
H4sIAAAAAAAC/w3Dhw0AIAwEsQ+9hpUZPWfJJikxs7CysXNwcnHz8NL5aNIPlvf4ekAAAAA=
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="4" name="zlib" width="4" height="4">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJwNw4cNACAMBLEPvYaVGT1nySYpMbOwsrFzcHJx8/DS+WjSDw1EAPo=
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
15
assets/tiled-fixtures/handmade.tsx
Normal file
15
assets/tiled-fixtures/handmade.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored fixture for the Ludic Tiled reader (issue #67). Public domain (CC0). -->
|
||||||
|
<tileset version="1.10" tiledversion="1.10.2" name="handmade" tilewidth="16" tileheight="16" spacing="0" margin="0" tilecount="16" columns="4">
|
||||||
|
<image source="handmade.png" width="64" height="64"/>
|
||||||
|
<tile id="4">
|
||||||
|
<properties>
|
||||||
|
<property name="solid" type="bool" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</tile>
|
||||||
|
<tile id="6">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" x="0" y="8" width="16" height="8"/>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
12
assets/tiled-fixtures/hexagonal-mini.tmx
Normal file
12
assets/tiled-fixtures/hexagonal-mini.tmx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.0" orientation="hexagonal" renderorder="right-down" width="20" height="20" tilewidth="14" tileheight="12" hexsidelength="6" staggeraxis="y" staggerindex="odd" nextobjectid="2">
|
||||||
|
<tileset firstgid="1" name="hex mini" tilewidth="18" tileheight="18">
|
||||||
|
<tileoffset x="0" y="1"/>
|
||||||
|
<image source="hexmini.png" width="106" height="72"/>
|
||||||
|
</tileset>
|
||||||
|
<layer name="Ground" width="20" height="20">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJyl1FEKhDAMBNBSt6jVaL3/Za2QwDAkVdiPQda2zyTonimlU1N6Ws+lkZ6l56AUXcPY2qlniv5uL5Z5BdyDvFXXMoX3Rp44axl6nqFejj3LLK6xgmf3Zg06Qs+O+qiaDOZOVgXPs7jfCme8Hkce1+fNlGdlM3myDTzc580fz1htW2Baj15/R/J72wLvcVZN5HnzGnmVPJ5hNH+0dt33j4ex91TARUs+WjNZz/fewKvJfy+/1naR+dX7OfdEnUYefyOeZZ7Vht/b5HjefxJbO1iTE7YWuEpg5hfPzi8D782x3Mg7DV4=
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
18
assets/tiled-fixtures/img_group.tmx
Normal file
18
assets/tiled-fixtures/img_group.tmx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P5 fixture: image + group layers (issue #73). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="anim_tiles.tsx"/>
|
||||||
|
<imagelayer id="1" name="bg" offsetx="8" offsety="4" repeatx="1">
|
||||||
|
<image source="../kenney/tiny-dungeon/Tilemap/tilemap.png" width="203" height="186"/>
|
||||||
|
</imagelayer>
|
||||||
|
<group id="2" name="grp" offsetx="16" opacity="0.5" tintcolor="#ff0000">
|
||||||
|
<layer id="3" name="inner" width="4" height="4">
|
||||||
|
<data encoding="csv">
|
||||||
|
2,0,0,0,
|
||||||
|
0,0,0,0,
|
||||||
|
0,0,0,0,
|
||||||
|
0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</group>
|
||||||
|
</map>
|
||||||
1
assets/tiled-fixtures/infinite_map.tmj
Normal file
1
assets/tiled-fixtures/infinite_map.tmj
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"type": "map", "version": "1.10", "orientation": "orthogonal", "renderorder": "right-down", "width": 0, "height": 0, "tilewidth": 16, "tileheight": 16, "infinite": true, "tilesets": [{"firstgid": 1, "source": "collision.tsx"}], "layers": [{"type": "tilelayer", "id": 1, "name": "ground", "width": 0, "height": 0, "startx": 0, "starty": 0, "chunks": [{"x": 0, "y": 0, "width": 16, "height": 16, "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]}, {"x": 16, "y": 0, "width": 16, "height": 16, "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]}]}]}
|
||||||
15
assets/tiled-fixtures/infinite_map.tmx
Normal file
15
assets/tiled-fixtures/infinite_map.tmx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P6 infinite/chunked fixture (issue #74). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="0" height="0" tilewidth="16" tileheight="16" infinite="1" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="collision.tsx"/>
|
||||||
|
<layer id="1" name="ground" width="0" height="0">
|
||||||
|
<data encoding="csv">
|
||||||
|
<chunk x="0" y="0" width="16" height="16">
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3
|
||||||
|
</chunk>
|
||||||
|
<chunk x="16" y="0" width="16" height="16">
|
||||||
|
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
|
||||||
|
</chunk>
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
43
assets/tiled-fixtures/isometric_grass_and_water.tmx
Normal file
43
assets/tiled-fixtures/isometric_grass_and_water.tmx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.4" tiledversion="1.4.3" orientation="isometric" renderorder="right-down" width="25" height="25" tilewidth="64" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" name="isometric_grass_and_water" tilewidth="64" tileheight="64" tilecount="24" columns="4">
|
||||||
|
<tileoffset x="0" y="16"/>
|
||||||
|
<grid orientation="isometric" width="64" height="32"/>
|
||||||
|
<image source="isometric_grass_and_water.png" width="256" height="384"/>
|
||||||
|
<wangsets>
|
||||||
|
<wangset name="Grass and Water" type="corner" tile="15">
|
||||||
|
<wangcolor name="Grass" color="#8ab022" tile="0" probability="1"/>
|
||||||
|
<wangcolor name="Water" color="#378dc2" tile="23" probability="1"/>
|
||||||
|
<wangtile tileid="0" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="1" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="2" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="3" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="4" wangid="0,1,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="5" wangid="0,1,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="6" wangid="0,1,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="7" wangid="0,2,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="8" wangid="0,2,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="9" wangid="0,1,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="10" wangid="0,2,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="11" wangid="0,2,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="12" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="13" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="14" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="15" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="16" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="17" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="18" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="19" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="20" wangid="0,2,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="21" wangid="0,1,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="22" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="23" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
</wangset>
|
||||||
|
</wangsets>
|
||||||
|
</tileset>
|
||||||
|
<layer id="1" name="Tile Layer 1" width="25" height="25">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJx1lttywjAMROVgyqVtAoFC/v9L68xoh5PFPGhIYktrrVYyS0QszZ7Nvpvd0n7y24L1Q7MhrTSreN/le821HZ7lv9qYa6sdE0cYs/kX7PXYwtfaevYp7WDrd+SnHByjYr/npP1zZ4/elcuM71rjeckdc5KNHX75fMwc9s2uzb6AsYstJzwrv5/Tz89SLIZy8v203llV8xl7yMU+462/v81OqA114/UhrzUxRqwprnh6ZGzp2PNQfPqRu/X9hnMV8F/xLg1L42erDf2oaa2RI2qPtbgbhmw2H69nMUxx/gVccXdC3AW/o/HV60vW59Lhu8arDxmfGIPFUV1qbLVQEIs4PlOeHQxqVjmzr5mLYsmf+5Qj5yM1r3Ne4p1D5VcMh3qWZibLx2fYkBhPYOv81I9wbrGd45zFU7zrndpwDjkHXXfej9zHc3EG+D3AWcCZMJif7hTnVxr6i9edtoBDz8N7kxqbY6sN9gJnsnqIOqCme7Un76579sIV8dccHvHqZefH76BP9wjzkVapM2rL+5/8cR6QS9eh8p2AT12y5oO9+7yh5hzLZypnHX29/pzB9PE7bOg8Mza5KvGu4R7mp/89zqvr7x+TnxEn
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
285
assets/tiled-fixtures/orthogonal-outside.tmx
Normal file
285
assets/tiled-fixtures/orthogonal-outside.tmx
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.8" tiledversion="1.8.5" orientation="orthogonal" renderorder="right-down" width="45" height="31" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="38">
|
||||||
|
<properties>
|
||||||
|
<property name="enemyTint" type="color" value="#ffa33636"/>
|
||||||
|
</properties>
|
||||||
|
<tileset firstgid="1" name="outdoor" tilewidth="16" tileheight="16" tilecount="288" columns="24">
|
||||||
|
<image source="buch-outdoor.png" width="384" height="192"/>
|
||||||
|
<tile id="6" probability="0.1"/>
|
||||||
|
<tile id="27" probability="0.05"/>
|
||||||
|
<tile id="28" probability="0.05"/>
|
||||||
|
<tile id="30" probability="0.1"/>
|
||||||
|
<tile id="51" probability="0.05"/>
|
||||||
|
<tile id="52" probability="0.05"/>
|
||||||
|
<tile id="54" probability="0.1"/>
|
||||||
|
<tile id="75" probability="0.05"/>
|
||||||
|
<tile id="76" probability="0.05"/>
|
||||||
|
<tile id="78" probability="0.1"/>
|
||||||
|
<tile id="82" probability="0.1"/>
|
||||||
|
<tile id="83" probability="0.1"/>
|
||||||
|
<tile id="99" probability="0.05"/>
|
||||||
|
<tile id="102" probability="0.1"/>
|
||||||
|
<tile id="106" probability="0.1"/>
|
||||||
|
<tile id="107" probability="0.1"/>
|
||||||
|
<tile id="126" probability="0.1"/>
|
||||||
|
<wangsets>
|
||||||
|
<wangset name="Terrains" type="corner" tile="25">
|
||||||
|
<wangcolor name="Grass" color="#fce94f" tile="150" probability="1"/>
|
||||||
|
<wangcolor name="Dirt" color="#ef2929" tile="100" probability="1"/>
|
||||||
|
<wangcolor name="Dark Dirt" color="#f57900" tile="34" probability="1"/>
|
||||||
|
<wangcolor name="Water" color="#729fcf" tile="171" probability="1"/>
|
||||||
|
<wangtile tileid="0" wangid="0,1,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="1" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="2" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="3" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="4" wangid="0,1,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="5" wangid="0,1,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="6" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="7" wangid="0,2,0,3,0,2,0,2"/>
|
||||||
|
<wangtile tileid="8" wangid="0,2,0,3,0,3,0,2"/>
|
||||||
|
<wangtile tileid="9" wangid="0,2,0,3,0,3,0,2"/>
|
||||||
|
<wangtile tileid="10" wangid="0,2,0,3,0,3,0,2"/>
|
||||||
|
<wangtile tileid="11" wangid="0,2,0,3,0,3,0,2"/>
|
||||||
|
<wangtile tileid="12" wangid="0,2,0,2,0,3,0,2"/>
|
||||||
|
<wangtile tileid="13" wangid="0,1,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="14" wangid="0,1,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="15" wangid="0,1,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="16" wangid="0,1,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="17" wangid="0,1,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="18" wangid="0,1,0,1,0,3,0,1"/>
|
||||||
|
<wangtile tileid="24" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="25" wangid="0,2,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="26" wangid="0,2,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="27" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="28" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="29" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="30" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="31" wangid="0,3,0,3,0,2,0,2"/>
|
||||||
|
<wangtile tileid="32" wangid="0,3,0,2,0,3,0,3"/>
|
||||||
|
<wangtile tileid="33" wangid="0,3,0,3,0,2,0,3"/>
|
||||||
|
<wangtile tileid="34" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="35" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="36" wangid="0,2,0,2,0,3,0,3"/>
|
||||||
|
<wangtile tileid="37" wangid="0,3,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="38" wangid="0,3,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="39" wangid="0,3,0,3,0,1,0,3"/>
|
||||||
|
<wangtile tileid="40" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="42" wangid="0,1,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="48" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="49" wangid="0,1,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="50" wangid="0,2,0,2,0,2,0,1"/>
|
||||||
|
<wangtile tileid="51" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="52" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="53" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="54" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="55" wangid="0,3,0,3,0,2,0,2"/>
|
||||||
|
<wangtile tileid="56" wangid="0,2,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="57" wangid="0,3,0,3,0,3,0,2"/>
|
||||||
|
<wangtile tileid="58" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="59" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="60" wangid="0,2,0,2,0,3,0,3"/>
|
||||||
|
<wangtile tileid="61" wangid="0,3,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="62" wangid="0,1,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="63" wangid="0,3,0,3,0,3,0,1"/>
|
||||||
|
<wangtile tileid="66" wangid="0,1,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="72" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="73" wangid="0,2,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="74" wangid="0,1,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="75" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="76" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="77" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="78" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="79" wangid="0,3,0,3,0,2,0,2"/>
|
||||||
|
<wangtile tileid="80" wangid="0,3,0,2,0,3,0,2"/>
|
||||||
|
<wangtile tileid="81" wangid="0,2,0,3,0,2,0,3"/>
|
||||||
|
<wangtile tileid="82" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="83" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="84" wangid="0,2,0,2,0,3,0,3"/>
|
||||||
|
<wangtile tileid="85" wangid="0,3,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="86" wangid="0,3,0,1,0,3,0,1"/>
|
||||||
|
<wangtile tileid="87" wangid="0,1,0,3,0,1,0,3"/>
|
||||||
|
<wangtile tileid="90" wangid="0,1,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="96" wangid="0,2,0,2,0,1,0,1"/>
|
||||||
|
<wangtile tileid="97" wangid="0,1,0,2,0,1,0,2"/>
|
||||||
|
<wangtile tileid="98" wangid="0,2,0,1,0,2,0,1"/>
|
||||||
|
<wangtile tileid="99" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="100" wangid="0,2,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="101" wangid="0,1,0,1,0,2,0,2"/>
|
||||||
|
<wangtile tileid="102" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="103" wangid="0,3,0,3,0,2,0,2"/>
|
||||||
|
<wangtile tileid="104" wangid="0,2,0,3,0,2,0,3"/>
|
||||||
|
<wangtile tileid="105" wangid="0,3,0,2,0,3,0,2"/>
|
||||||
|
<wangtile tileid="106" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="107" wangid="0,3,0,3,0,3,0,3"/>
|
||||||
|
<wangtile tileid="108" wangid="0,2,0,2,0,3,0,3"/>
|
||||||
|
<wangtile tileid="109" wangid="0,3,0,3,0,1,0,1"/>
|
||||||
|
<wangtile tileid="110" wangid="0,1,0,3,0,1,0,3"/>
|
||||||
|
<wangtile tileid="111" wangid="0,3,0,1,0,3,0,1"/>
|
||||||
|
<wangtile tileid="114" wangid="0,1,0,1,0,3,0,3"/>
|
||||||
|
<wangtile tileid="120" wangid="0,2,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="121" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="122" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="123" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="124" wangid="0,2,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="125" wangid="0,1,0,1,0,1,0,2"/>
|
||||||
|
<wangtile tileid="126" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="127" wangid="0,3,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="128" wangid="0,3,0,2,0,2,0,3"/>
|
||||||
|
<wangtile tileid="129" wangid="0,3,0,2,0,2,0,3"/>
|
||||||
|
<wangtile tileid="130" wangid="0,3,0,2,0,2,0,3"/>
|
||||||
|
<wangtile tileid="131" wangid="0,3,0,2,0,2,0,3"/>
|
||||||
|
<wangtile tileid="132" wangid="0,2,0,2,0,2,0,3"/>
|
||||||
|
<wangtile tileid="133" wangid="0,3,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="134" wangid="0,3,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="135" wangid="0,3,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="136" wangid="0,3,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="137" wangid="0,3,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="138" wangid="0,1,0,1,0,1,0,3"/>
|
||||||
|
<wangtile tileid="144" wangid="0,1,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="145" wangid="0,1,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="146" wangid="0,1,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="147" wangid="0,1,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="148" wangid="0,1,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="149" wangid="0,1,0,1,0,4,0,1"/>
|
||||||
|
<wangtile tileid="150" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="151" wangid="0,2,0,4,0,2,0,2"/>
|
||||||
|
<wangtile tileid="152" wangid="0,2,0,4,0,4,0,2"/>
|
||||||
|
<wangtile tileid="153" wangid="0,2,0,4,0,4,0,2"/>
|
||||||
|
<wangtile tileid="154" wangid="0,2,0,4,0,4,0,2"/>
|
||||||
|
<wangtile tileid="155" wangid="0,2,0,4,0,4,0,2"/>
|
||||||
|
<wangtile tileid="156" wangid="0,2,0,2,0,4,0,2"/>
|
||||||
|
<wangtile tileid="168" wangid="0,4,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="169" wangid="0,4,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="170" wangid="0,4,0,4,0,1,0,4"/>
|
||||||
|
<wangtile tileid="171" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="172" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="173" wangid="0,1,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="174" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="175" wangid="0,4,0,4,0,2,0,2"/>
|
||||||
|
<wangtile tileid="176" wangid="0,4,0,2,0,4,0,4"/>
|
||||||
|
<wangtile tileid="177" wangid="0,4,0,4,0,2,0,4"/>
|
||||||
|
<wangtile tileid="178" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="179" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="180" wangid="0,2,0,2,0,4,0,4"/>
|
||||||
|
<wangtile tileid="192" wangid="0,4,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="193" wangid="0,1,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="194" wangid="0,4,0,4,0,4,0,1"/>
|
||||||
|
<wangtile tileid="195" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="196" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="197" wangid="0,1,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="198" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="199" wangid="0,4,0,4,0,2,0,2"/>
|
||||||
|
<wangtile tileid="200" wangid="0,2,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="201" wangid="0,4,0,4,0,4,0,2"/>
|
||||||
|
<wangtile tileid="202" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="203" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="204" wangid="0,2,0,2,0,4,0,4"/>
|
||||||
|
<wangtile tileid="216" wangid="0,4,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="217" wangid="0,4,0,1,0,4,0,1"/>
|
||||||
|
<wangtile tileid="218" wangid="0,1,0,4,0,1,0,4"/>
|
||||||
|
<wangtile tileid="219" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="220" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="221" wangid="0,1,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="222" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="223" wangid="0,4,0,4,0,2,0,2"/>
|
||||||
|
<wangtile tileid="224" wangid="0,4,0,2,0,4,0,2"/>
|
||||||
|
<wangtile tileid="225" wangid="0,2,0,4,0,2,0,4"/>
|
||||||
|
<wangtile tileid="226" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="227" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="228" wangid="0,2,0,2,0,4,0,4"/>
|
||||||
|
<wangtile tileid="240" wangid="0,4,0,4,0,1,0,1"/>
|
||||||
|
<wangtile tileid="241" wangid="0,1,0,4,0,1,0,4"/>
|
||||||
|
<wangtile tileid="242" wangid="0,4,0,1,0,4,0,1"/>
|
||||||
|
<wangtile tileid="243" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="244" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="245" wangid="0,1,0,1,0,4,0,4"/>
|
||||||
|
<wangtile tileid="246" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="247" wangid="0,4,0,4,0,2,0,2"/>
|
||||||
|
<wangtile tileid="248" wangid="0,2,0,4,0,2,0,4"/>
|
||||||
|
<wangtile tileid="249" wangid="0,4,0,2,0,4,0,2"/>
|
||||||
|
<wangtile tileid="250" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="251" wangid="0,4,0,4,0,4,0,4"/>
|
||||||
|
<wangtile tileid="252" wangid="0,2,0,2,0,4,0,4"/>
|
||||||
|
<wangtile tileid="264" wangid="0,4,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="265" wangid="0,4,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="266" wangid="0,4,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="267" wangid="0,4,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="268" wangid="0,4,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="269" wangid="0,1,0,1,0,1,0,4"/>
|
||||||
|
<wangtile tileid="270" wangid="0,1,0,1,0,1,0,1"/>
|
||||||
|
<wangtile tileid="271" wangid="0,4,0,2,0,2,0,2"/>
|
||||||
|
<wangtile tileid="272" wangid="0,4,0,2,0,2,0,4"/>
|
||||||
|
<wangtile tileid="273" wangid="0,4,0,2,0,2,0,4"/>
|
||||||
|
<wangtile tileid="274" wangid="0,4,0,2,0,2,0,4"/>
|
||||||
|
<wangtile tileid="275" wangid="0,4,0,2,0,2,0,4"/>
|
||||||
|
<wangtile tileid="276" wangid="0,2,0,2,0,2,0,4"/>
|
||||||
|
</wangset>
|
||||||
|
</wangsets>
|
||||||
|
</tileset>
|
||||||
|
<layer id="1" name="Ground" width="45" height="31">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJyNWE1vVVUU3Y0KQeXL4kBL7QAiEkcopQOIkjgysXagYeLIpE0HGucEtY5U6giw1Bj8AdiWxhZ+ANLS4nv+AJKW1+TVH9DklWfSR+LeeWt51j3eWxmsnPvuPR9rr7P3Pvu8hpkd7DFb8dYb24Pf89bFtOOio8/xquNreZ/jdccxx0nH53j+SN7F81nHOfz+BGs1vP0C706i/1kZ+6asMWRdrvMY+4q3bzmedTwj/FfAO/i2HVcc10s4f+t4QzidcEw6vnd8lz0HfrAu3wOO8xj3seO44AT6cY0GONPWTx1jjp8cvzjO4Fsbfan/luP3XXgrJ+4XtWnjeQX9VzBmErgsdud8lXdwCb94G5w3HE+g+R5ZcwDPDx114U3dY569wkW5UZ8J8I6+AfalPbT7MvhS03nMMV3CNzT+A5zHMt70obvezjrWwfue47HwynUhH3JivwG01E1Be0cs8eW3mOd9x2lwXHVsOx7AP8bgI/PCZ5+PmwHvHceSaK76UZO2cGyg1X2mT/eKrtOwUX2Le/RcicbB4zfHyz6239sPs3VijtB6E33XRPPr0CbXmpwaAvrEPPyBvsDvOn4IbQ+4jkHnDaxfE52POl6zFIe0n1oH12Vw37Hq2HzH8W5o6DgE9FrRn+l7uk8rVvSZi6LvDdF4GaBPU2edS32kDjub1vWrMt6Mq8gJkc/OwQbypq9T54jrKcfVkrl+Fq5ctwn9/hTOZefIgPC+7VhEW9tFb+bwyL2Rw3le5DHGeVuY6xrma0DPJtpYL+Ip/JT+TM7uT9/k649jji3MEWMO+5gF2Py4hLOel8y/A1bMKdRbeW8K99BkzlIOmMVe37GuL2u+y/PYCNbbAkfGwCo0vyX9cq0nwbcsXnP/e97xAjADzutoQ6def3+kJ2lMnCmZL/wl/C7y87bsTXDuOB5Bt//jpbgiflBlQwe6am4bE40Dw6J1PscW9inmiJhlvvzVcaAn7fnT8JyC/ZG77u/SX3Nx8CPnQNRxl6xY05XZPYN5HoBv+F+VL49kccF9on9GG757q2J84KXMD0bBrw/+EHt7yro5umqOh7IW1330FHtNvkvQdg72co8GKtajnn2ib+QJ1p7T2XMgzlDPI//mv+C8Db5NWVO/MeangB/Bt4Y9CnuXs7FVIEfWofy9d5cxPF8mRKtNaDQLvxiy5O/xvgMteQ5si7Y1PLdkf8owjvW+hIan7L85gnXJRLd/wQ+ZD+jPjOU1K9ao1JLfeWatgfdtvGcNsBtnzjcu+/+VdWMubNA6i3lNx9MWahl7qvUHa1RqS79pWjord9C/hedFjLsKaByQ72KJLax1437GmqZX+OV2Mz+FP9y0dI52wKuFd3y/CH3Zh1qz1f7x7n62X/USHm3ggqU7pdYyeZ57EedTxI3W1XXw25T1a+DFs4vn/Bz4d7BHm5bOOe4XvzdLeBBRO0YdwzszeYevjFvymYg1xiv1qFvKtx1o17QUi1EXMHYOgfsR8G9Z8hXWiZyrjvFVnFl/kfOxjDdrmeAZeeuapTOce8x6gHquAVEv6jnL51VozH6shdbAn3VnrBvxdtCS7/LdeUv/NUQNxntlG7ypK/dxHWuw/tbzlTVIrMlaKu4Zeu4egm30I+a/WbTL+KaaHoeWh8GX+sZ/I1HnDlixFr9nqY5dtFQztjK+vMurbsH7hqU7J/X+GxyXMGcNWJI9oy9csOJ/PKEr/0cJe96Dvjdl/3g/2ZZ1uH9/Wbe2GnR8AC7Rb8dSPOV2jaIfc7ae6ew7akX/nQRP+kC8j/OO98vgvA95Yj/q2v2SK7Sm5fx5rC2gT/R/YsXacRT7EeMXLMVdcNa4rYrByMd6B676j44+3QFn1U7jTLkzz6nGw/I93m9Yyot5HcdzhPkrELkrYjD8mnGZc2a9pnUO+Q5mPEO/fivG21Grtktty+8eqjM58b873tk+s25c6t0+cNdS/cAYv1Oxfhn6pR20oh+dzvoOZ/b+A6OnAUo=
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="Fringe" width="45" height="31">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJzVl9lNw0AURZ8ltgr4YquAjliaYPl8DdAChCVABRB2KkBhpwK2sKQC4FgiShQ8tseMx/aVjmRLY+fO3JnnFxGRGZiVamkBFos2EaOBQGQw6N6PFmcltcbwOx50vU+neKYztsb1OmzAZq4uo9XxbjO2wfUBHMJRjt5c6RyacAlXcF2oGzu9wCu0YCohpwcPftJqFdZ+r9uGMf1nvWjtwX7P/UiEN5vzUiZRW5Qao9QarUHRftKI2qLUGKXWaCOjZ9/zprYoNUapNdrM+Hsu5m0jaotSY5Rao62M73Ax70d4gme4yOgjSb01tH/eu/947xd8izvfS7Ds6F2+VZP8e5oVx+8rY0/D90dN385QPnuapN7DZnxvT1O2HiCNqtYD2GaXt3aot9tQh62K9CJn+DyFEziuiOd7fN7BLdyUxDN5C7kL+Yf74I8+8fkB7/AGQ+zdYU/7t21YI/IWchfyD/dBoibwO1nwmSNvIXch/3AfxCopE18ibyF3If9wH8TKNhOfMq2nTSZ5yXSeTOvZn0nU83Mw78Bb1P/tUKbzlHY9k87jD20Li3Y=
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<objectgroup id="3" name="Objects">
|
||||||
|
<object id="1" name="maggots" type="Location" x="435" y="74" width="155" height="99">
|
||||||
|
<properties>
|
||||||
|
<property name="spawncount" type="int" value="5"/>
|
||||||
|
<property name="spawntype" value="maggot"/>
|
||||||
|
</properties>
|
||||||
|
</object>
|
||||||
|
<object id="2" name="discover chest" type="Trigger" x="201" y="200" width="127" height="127">
|
||||||
|
<properties>
|
||||||
|
<property name="script" type="file" value="chest-discovered.lua"/>
|
||||||
|
</properties>
|
||||||
|
<ellipse/>
|
||||||
|
</object>
|
||||||
|
<object id="3" name="unreachable" type="Fixture" x="2" y="158">
|
||||||
|
<properties>
|
||||||
|
<property name="static" type="bool" value="true"/>
|
||||||
|
</properties>
|
||||||
|
<polygon points="0,0 55,-23 96,-117 110,-61 104,-42 119,-33 116,6 104,9 100,36 60,43 53,58 43,58 34,74 21,69 18,90 0,89"/>
|
||||||
|
</object>
|
||||||
|
<object id="5" name="guard" type="NPC" x="22" y="361">
|
||||||
|
<polyline points="-3,120 87,91 154,96 181,16 273,-1"/>
|
||||||
|
</object>
|
||||||
|
<object id="6" name="guard" type="NPC" x="277" y="18">
|
||||||
|
<polyline points="0,0 75,78 133,82 176,179 274,183"/>
|
||||||
|
</object>
|
||||||
|
<object id="10" gid="282" x="413.333" y="225.333" width="16" height="16"/>
|
||||||
|
<object id="11" gid="282" x="421.667" y="218" width="16" height="16"/>
|
||||||
|
<object id="12" gid="2147483930" x="423" y="235.333" width="16" height="16"/>
|
||||||
|
<object id="13" gid="282" x="5" y="70" width="16" height="16"/>
|
||||||
|
<object id="14" gid="282" x="-3.66667" y="80.3333" width="16" height="16"/>
|
||||||
|
<object id="16" gid="283" x="538" y="418.333" width="16" height="16"/>
|
||||||
|
<object id="17" gid="283" x="407.667" y="462" width="16" height="16"/>
|
||||||
|
<object id="18" gid="283" x="417" y="473.667" width="16" height="16"/>
|
||||||
|
<object id="19" gid="283" x="402.667" y="469" width="16" height="16"/>
|
||||||
|
<object id="21" gid="2147483930" x="683.333" y="260.5" width="16" height="16"/>
|
||||||
|
<object id="22" gid="282" x="692.167" y="269.167" width="16" height="16"/>
|
||||||
|
<object id="23" gid="282" x="701.667" y="247.833" width="16" height="16"/>
|
||||||
|
<object id="24" gid="282" x="688.5" y="242" width="16" height="16"/>
|
||||||
|
<object id="25" gid="282" x="670.5" y="263.5" width="16" height="16"/>
|
||||||
|
<object id="26" gid="282" x="680" y="284" width="16" height="16"/>
|
||||||
|
<object id="27" gid="282" x="643.833" y="283.667" width="16" height="16"/>
|
||||||
|
<object id="28" gid="282" x="63.4165" y="386" width="16" height="16"/>
|
||||||
|
<object id="29" gid="282" x="9.0835" y="356.167" width="16" height="16"/>
|
||||||
|
<object id="30" gid="282" x="11.9165" y="385" width="16" height="16"/>
|
||||||
|
<object id="31" gid="282" x="54.2495" y="378.5" width="16" height="16"/>
|
||||||
|
<object id="32" gid="2147483930" x="2.4165" y="364.5" width="16" height="16"/>
|
||||||
|
<object id="33" gid="2147483930" x="41.5835" y="382.833" width="16" height="16"/>
|
||||||
|
<object id="34" type="Sign" gid="257" x="670.667" y="87" width="16" height="16">
|
||||||
|
<properties>
|
||||||
|
<property name="text" value="East West"/>
|
||||||
|
</properties>
|
||||||
|
</object>
|
||||||
|
<object id="37" name="player-start" type="Location" x="192" y="160">
|
||||||
|
<point/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
47
assets/tiled-fixtures/p2_map.tmx
Normal file
47
assets/tiled-fixtures/p2_map.tmx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P2 collision fixture (issue #70). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="16" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="p2_tiles.tsx"/>
|
||||||
|
<layer id="1" name="collision" width="16" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="props" width="16" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="3" name="floor" width="16" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
12
assets/tiled-fixtures/p2_tiles.tsx
Normal file
12
assets/tiled-fixtures/p2_tiles.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored collision-metadata tileset (issue #70). CC0. -->
|
||||||
|
<tileset version="1.10" tiledversion="1.10.2" name="p2tiles" tilewidth="16" tileheight="16" tilecount="5" columns="5">
|
||||||
|
<tile id="1">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" x="0" y="0" width="16" height="16"/>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
<tile id="2"><properties><property name="oneway" type="bool" value="true"/></properties></tile>
|
||||||
|
<tile id="3"><properties><property name="solid" type="bool" value="true"/></properties></tile>
|
||||||
|
<tile id="4"><properties><property name="trigger" type="bool" value="true"/></properties></tile>
|
||||||
|
</tileset>
|
||||||
8
assets/tiled-fixtures/p4_enemy.tx
Normal file
8
assets/tiled-fixtures/p4_enemy.tx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<template>
|
||||||
|
<object type="Enemy" width="16" height="16">
|
||||||
|
<properties>
|
||||||
|
<property name="hp" type="int" value="25"/>
|
||||||
|
</properties>
|
||||||
|
</object>
|
||||||
|
</template>
|
||||||
19
assets/tiled-fixtures/p4_map.tmx
Normal file
19
assets/tiled-fixtures/p4_map.tmx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P4 fixture: object shapes, custom types, templates (issue #72). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="13">
|
||||||
|
<objectgroup id="1" name="shapes">
|
||||||
|
<object id="1" x="10" y="10" width="20" height="30"/>
|
||||||
|
<object id="2" x="40" y="10" width="20" height="20"><ellipse/></object>
|
||||||
|
<object id="3" x="70" y="10"><point/></object>
|
||||||
|
<object id="4" x="90" y="10"><polygon points="0,0 16,0 16,16 0,16"/></object>
|
||||||
|
<object id="5" x="120" y="10"><polyline points="0,0 10,10 20,0"/></object>
|
||||||
|
<object id="6" x="10" y="60" width="80" height="20"><text pixelsize="12" halign="center">Hello</text></object>
|
||||||
|
</objectgroup>
|
||||||
|
<objectgroup id="2" name="spawns">
|
||||||
|
<object id="10" type="Enemy" x="32" y="48" width="16" height="16">
|
||||||
|
<properties><property name="hp" type="int" value="99"/></properties>
|
||||||
|
</object>
|
||||||
|
<object id="11" type="Enemy" x="64" y="48" width="16" height="16"/>
|
||||||
|
<object id="12" template="p4_enemy.tx" x="80" y="48"/>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
8
assets/tiled-fixtures/p4_types.xml
Normal file
8
assets/tiled-fixtures/p4_types.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<objecttypes>
|
||||||
|
<objecttype name="Enemy" color="#ff0000">
|
||||||
|
<property name="hp" type="int" default="10"/>
|
||||||
|
<property name="speed" type="int" default="3"/>
|
||||||
|
<property name="boss" type="bool" default="false"/>
|
||||||
|
</objecttype>
|
||||||
|
</objecttypes>
|
||||||
20
assets/tiled-fixtures/perspective_walls.tsx
Normal file
20
assets/tiled-fixtures/perspective_walls.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset name="perspective_walls" tilewidth="64" tileheight="64">
|
||||||
|
<tileoffset x="-32" y="0"/>
|
||||||
|
<image source="perspective_walls.png"/>
|
||||||
|
<tile id="13">
|
||||||
|
<properties>
|
||||||
|
<property name="door" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</tile>
|
||||||
|
<tile id="14">
|
||||||
|
<properties>
|
||||||
|
<property name="door" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</tile>
|
||||||
|
<tile id="15">
|
||||||
|
<properties>
|
||||||
|
<property name="pickup" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
19
assets/tiled-fixtures/physics_map.tmx
Normal file
19
assets/tiled-fixtures/physics_map.tmx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored map for the Ludic Tiled demos (issue #69). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="16" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="collision.tsx"/>
|
||||||
|
<layer id="1" name="collision" width="16" height="10">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,
|
||||||
|
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
16
assets/tiled-fixtures/sewers.tmx
Normal file
16
assets/tiled-fixtures/sewers.tmx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.0" orientation="orthogonal" width="50" height="50" tilewidth="24" tileheight="24">
|
||||||
|
<tileset firstgid="1" name="sewer_tileset" tilewidth="24" tileheight="24">
|
||||||
|
<image source="sewer_tileset.png" trans="ff00ff" width="192" height="217"/>
|
||||||
|
</tileset>
|
||||||
|
<layer name="Bottom" width="50" height="50">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJzt19kKwjAQBdDim0sFqwguL3Vf/sP//ySnkIFhSGrSdEnxPhyQxqJ32kySPMuyJTkZC5KLa2dyNEo1Lsds3wkl/4f+7V/0/f+Y40Je5GpUn2+J5NiQCdmO/HlMyYzMI3JwLUJx7drIsSIFWUfk4FrY3GvGuHb8vr4jcmhNcnAtpIflmmar3VA5nqaWMUKeQ1d9dyht59iRPTmMPEdpua8Put/Frh88P5q84zFkv/NZP2TOlOaH7Hc+64fMmdL8cPVbV9+tcn5MzpTmR0gv12uDbQ4MNT8AIA73Uq3v3hqLe2ldTx4D21k1tf2ubw59Vk1tX+KbQ59VXfuSlMn9SJHV70tS5jqrYu8BAAAAAAAAAABd+wIHfQq1
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer name="Top" width="50" height="50" opacity="0.49">
|
||||||
|
<data encoding="base64" compression="zlib">
|
||||||
|
eJzt1jsKgDAQQEELtVKvYuGvEDvvfya3MBeQQAzMwCPdsum2af5lL71AJv7xL7X+Y46WtzXayq7z2RGd0f2+V6a5bdRFfaZ5pQzRGE2lFwEAoArpDk7Veg+nOzjlHgYAAAAAIIcHvboDlQ==
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
15
assets/tiled-fixtures/zstd_map.tmx
Normal file
15
assets/tiled-fixtures/zstd_map.tmx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P6 zstd fixture (issue #74). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="24" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="collision.tsx"/>
|
||||||
|
<layer id="1" name="csv" width="24" height="16">
|
||||||
|
<data encoding="csv">
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="2" name="zstd" width="24" height="16">
|
||||||
|
<data encoding="base64" compression="zstd">
|
||||||
|
KLUv/WAABe0AADgBAAAAAwIBBgC4mgT54KqgO9JxR0xFgOm26RIB
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Changesets
|
# Changesets
|
||||||
|
|
||||||
A **changeset** is one small Markdown file describing a single user-facing change,
|
A **changeset** is one small Markdown file describing a single user-facing change,
|
||||||
dropped in this directory. `x release` consumes every changeset here into a new
|
dropped in this directory. `ludic-dev release` consumes every changeset here into a new
|
||||||
`CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files.
|
`CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files.
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
@ -14,12 +14,38 @@ the changelog. Markdown is fine.
|
||||||
```
|
```
|
||||||
|
|
||||||
- `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped
|
- `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped
|
||||||
by the **highest** level among the pending changesets (unless `x release <level>`
|
by the **highest** level among the pending changesets (unless `ludic-dev release <level>`
|
||||||
overrides it).
|
overrides it).
|
||||||
- `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …); it
|
- `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It
|
||||||
becomes the bold prefix of the changelog bullet.
|
decides which group the change lands in: `feat` → **Features**, `fix` →
|
||||||
|
**Fixes**, `perf` → **Performance**, and so on, in that order. A type with no
|
||||||
|
known heading gets one named after itself.
|
||||||
|
|
||||||
|
## Writing the body
|
||||||
|
|
||||||
|
The body is markdown and reaches the changelog as markdown: it becomes one list
|
||||||
|
item, with continuation lines indented to stay inside it. Nested bullets, blank
|
||||||
|
lines between paragraphs and inline code all survive.
|
||||||
|
|
||||||
|
```
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**Tiled map support** — load and draw Tiled maps.
|
||||||
|
|
||||||
|
- **TMX/TSX** — the XML formats, decoded to the same intermediate as JSON.
|
||||||
|
- **Collision** — the `collision` layer projects onto the engine tilemap.
|
||||||
|
```
|
||||||
|
|
||||||
|
Lead with the thing that changed, not with the mechanism. A reader scanning the
|
||||||
|
release should be able to stop after your first clause.
|
||||||
|
|
||||||
## Adding one
|
## Adding one
|
||||||
|
|
||||||
Create a file with a short, unique name, e.g. `changes/regex-namespace.md`. Any
|
Create a file with a short, unique name, e.g. `changes/regex-namespace.md`. Any
|
||||||
filename works except this `README.md`, which the release step always skips.
|
filename works except this `README.md`, which the release step always skips.
|
||||||
|
|
||||||
|
Preview how the next release will read before cutting it — this writes nothing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ludic-dev release --dry-run
|
||||||
|
```
|
||||||
|
|
|
||||||
8
changes/a-hidden-body-still-stands-in-the-sun.md
Normal file
8
changes/a-hidden-body-still-stands-in-the-sun.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
`Actor.cast_hidden` separates being DRAWN from CASTING. `ac_visible` rejected any hidden
|
||||||
|
actor from the shadow pass as well as the scene pass, so a game that hides the player's
|
||||||
|
own body - first person, or a viewfinder held to the eye - lost that player's shadow
|
||||||
|
entirely. An actor hidden because the camera is inside its head is still standing in the
|
||||||
|
sun; set this on it and it keeps its shadow. Everything else still stops casting when it
|
||||||
|
is hidden.
|
||||||
17
changes/a-leaf-is-not-matte.md
Normal file
17
changes/a-leaf-is-not-matte.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
A leaf is not matte.
|
||||||
|
|
||||||
|
Foliage roughness was pinned to 1.0 and grazing Fresnel switched off, so nothing green in
|
||||||
|
the game had a highlight anywhere: the glint off waxy leaves and wet needles, which is
|
||||||
|
most of what makes a real stand look alive rather than painted, was simply absent.
|
||||||
|
|
||||||
|
The reason it was switched off is real. A crown is card quads, and at a grazing angle the
|
||||||
|
card's normal is a lie, so a plain specular lobe frosted whole crowns white against the
|
||||||
|
sky. So the sheen comes back as its own term gated on exactly that: it fades out as the
|
||||||
|
card turns edge-on, which is where its normal stops meaning anything. A tight lobe for the
|
||||||
|
glint, a weak wide one for the waxy rim, and nothing at all at the angles that frosted.
|
||||||
|
|
||||||
|
Thin-leaf translucency reaches further with it - a backlit stand glows for as far as you
|
||||||
|
can see it, not 140 m - and a dense crown passes 0.45 of it rather than 0.3. The distance
|
||||||
|
cap that stops a two-pixel clump card becoming a lime disc stays.
|
||||||
28
changes/a-lens-and-an-edge.md
Normal file
28
changes/a-lens-and-an-edge.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Anti-aliasing that exists, and a lens for the viewfinder.
|
||||||
|
|
||||||
|
THE SHIPPING DEFAULT HAD NO ANTI-ALIASING AT ALL. The temporal resolve was removed (for
|
||||||
|
good reasons - it reprojected water through the surface plane and dragged the mirror image
|
||||||
|
behind the camera), the setting's first option went on saying "Temporal", and MSAA defaults
|
||||||
|
to one sample. Every machine without DLSS - which is every Mac - drew a frame full of grass
|
||||||
|
blades and needle cards with nothing smoothing a single edge.
|
||||||
|
|
||||||
|
FXAA now, in the sharpen pass, because that pass already reads this pixel's neighbourhood
|
||||||
|
and runs last on the LDR image. It has no history, so it cannot drag or smear a reflection.
|
||||||
|
Measured on edge pixels: 25.5% less single-pixel staircase.
|
||||||
|
|
||||||
|
One trap worth recording. The unsharp mask's delta is computed from the RAW image and only
|
||||||
|
then applied to the anti-aliased colour. Taking the centre from the FXAA result and the
|
||||||
|
neighbours from the raw texture measures a difference that is half smoothing and half
|
||||||
|
signal, so the mask sharpens exactly the edges FXAA just softened - measured, that first
|
||||||
|
version was 29% WORSE than no anti-aliasing at all.
|
||||||
|
|
||||||
|
And depth of field, for the photo mode: a disc of taps whose radius is the pixel's circle
|
||||||
|
of confusion, signed so the two sides of the focal plane differ and normalised by the focus
|
||||||
|
distance, because a lens focused at two metres throws a background out far harder than one
|
||||||
|
focused at two hundred. A tap only counts if it is at least as out of focus as the pixel
|
||||||
|
it is blurring into, which is what keeps a sharp foreground from haloing into a blurred
|
||||||
|
background. It runs between the scene and the bloom so a blurred highlight still blooms,
|
||||||
|
and the whole pass is skipped when the aperture is shut - in ordinary play there is no lens
|
||||||
|
and this never draws.
|
||||||
11
changes/actions.md
Normal file
11
changes/actions.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Actions and reducers.** `action PickUp { item: int }` is a typed record of something that
|
||||||
|
happened; `reducer Bag on PickUp(b: mut Bag, a: PickUp) { ... }`, in the module that owns the state,
|
||||||
|
says what it means for that one state - a reducer takes exactly its state and the action, and a
|
||||||
|
second state is refused; `dispatch PickUp { item: 7 }` queues one from anywhere. The queue is drained
|
||||||
|
at the end of every phase of the frame loop, after every phase of ludic.base's `core_tick_all`, and
|
||||||
|
where a program calls `drain_actions()`: in dispatch order, each action's reducers in the order of
|
||||||
|
their states' names, an action a reducer dispatches queued behind (a queue still growing after 64
|
||||||
|
rounds stops the program, naming the action). `ludic deps` reports `widest_function` - the most
|
||||||
|
states any function or entry point of the program takes - and `--check` ratchets it.
|
||||||
15
changes/an-aspen-quakes.md
Normal file
15
changes/an-aspen-quakes.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
An aspen leaf hangs on a flattened stalk and turns in air a spruce never feels, and the
|
||||||
|
kit had no way to say so. `layer_flutter(l, v)` gives a scatter layer a per-leaf tremble:
|
||||||
|
the vertex stage offsets each leaf by a phase taken from its own place on the card, so
|
||||||
|
neighbouring leaves are never in step, and writes the result out as a varying the
|
||||||
|
fragment stage uses to flash the leaf's pale underside as it turns. The flash is the part
|
||||||
|
that reads - a still frame of a tremble is a still frame of nothing. One uniform, one
|
||||||
|
varying, no extra pass, and every other layer leaves it at zero.
|
||||||
|
|
||||||
|
Also fixes the sway itself, which was measured in METRES: `hgt * hgt * 0.35` is right for
|
||||||
|
a 40 cm flower and puts ten metres of sideways into a 14 m trunk, so every tall tree in
|
||||||
|
the valley stood bent over like a fishing rod. It is a fraction of the model's own height
|
||||||
|
now, so the tip moves a few per cent of the tree whatever the tree is and the base does
|
||||||
|
not move at all.
|
||||||
9
changes/anim-ozz.md
Normal file
9
changes/anim-ozz.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic.anim` carries ozz-animation (0.17.0, MIT) as a native library**, the second package to do
|
||||||
|
so after `ludic.physics`. Its skeleton and each clip are built at LOAD from the numbers the package
|
||||||
|
already reads - a skin's parents and rest pose, a clip's flattened channels - so there is no bake
|
||||||
|
step and no new file. `anim_oz_skel(sk)`, `anim_oz_clip(c)`, `anim_oz_ctx(s)` and
|
||||||
|
`anim_oz_sample(ctx, clip, t, rot, pos, n)` are the first step (Maroon Lake's phase 19.1): a sampled
|
||||||
|
rotation agrees with `anim_mix` to within 1e-4 a component. `anim_play` is unchanged. The library is
|
||||||
|
built by `native/build.sh` from the pinned release, and on Windows it imports KERNEL32 alone.
|
||||||
6
changes/asset-map.md
Normal file
6
changes/asset-map.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`@Asset(kind, map)`: a path under each map's directory.** A field whose file lives in the map's own folder
|
||||||
|
(a grass kind's density picture, `ground/blades.png`) says so, and `ludicc --check` looks for it in every
|
||||||
|
map - a @PerMap row's in its map, a game-wide row's in all of them - refusing a map that lacks it unless the
|
||||||
|
field is `@Asset(kind, map, optional)`. The schema marks the attribute `"scope": "map"`.
|
||||||
7
changes/attrs-before-export.md
Normal file
7
changes/attrs-before-export.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**An attribute before `export` is kept.** `@ToClients export event E`, `@Sync export property P`,
|
||||||
|
`@Owned export model M` and the rest lost the attributes written in front of `export`: the parser read
|
||||||
|
them, then parsed the declaration afresh and forgot them - so an exported remote event was silently
|
||||||
|
local; and `export @ToClients event` was refused outright. Attributes and `export` now read in
|
||||||
|
either order into the same declaration.
|
||||||
8
changes/bake-expand.md
Normal file
8
changes/bake-expand.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**A bake's inputs can follow the data.** `bake_expand(inputs, map)` gives a Bakes row's inputs as they are
|
||||||
|
hashed: `{map}` put as the map's key, and each input with a `*` put as the paths it matches, sorted as whole
|
||||||
|
paths byte by byte, dot-names left out, a glob matching nothing gone. `bake_maps(first_input)` is the maps a
|
||||||
|
`{map}` row covers: each directory under assets/maps holding its first input (`bake_maps_in` under another
|
||||||
|
root). A runner hashes `bake_inputs_hash(bake_expand(row.inputs, map))`, the same path the check takes, so
|
||||||
|
the two cannot disagree on stale; a game's Python tools are its checked twin.
|
||||||
11
changes/bake-images.md
Normal file
11
changes/bake-images.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Bakes you can look at, and three more of the renderer's textures read from one.** `ludic.lab` writes
|
||||||
|
raw 8-bit pixels (1 to 4 channels) as a PNG (`lab_png_write`, `png_write.ludic`, importable alone with its
|
||||||
|
own `LabPngState`) and turns float textures into honest previews (`png_convert.ludic`: R32F min..max as
|
||||||
|
grey, RG16F as red and green x255, HDR RGBA16F as x/(1+x) then sRGB). `ludic.render3d` takes three bakes
|
||||||
|
the game names (`bake_load.ludic`) and makes each as before when one is missing or stale: an impostor's
|
||||||
|
atlases as BC7 with their baked mips, through the compressed upload (`impostor_from_baked`,
|
||||||
|
`impostor_fill_bc7`; a fog opening past its cards reads the bake again instead of painting), the sky's image-based light at the start yaw per prefilter width (`sky_baked_in`; any other
|
||||||
|
turn of the sky is convolved), and the grass carpet (`carpet_from_baked`, `carpet_bytes`).
|
||||||
|
`impostor_from_bytes` returns null, keeping nothing, when the bytes are not the impostor's shape.
|
||||||
8
changes/baked-textures.md
Normal file
8
changes/baked-textures.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Textures a game baked at build time are read before the PNG.** `png_decode` first takes
|
||||||
|
`assets/baked/png/<path>.tex` (a game's `ludic bake` output: the samples, ready to upload), and a cut-out
|
||||||
|
load (`tex_load_ex` with dilate) first takes `assets/baked/cutouts/<path>.bc7` (padded as `tex_dilate`
|
||||||
|
pads, then BC7 with its mips) - so a boot decodes, pads and converts nothing it can take ready-made. Both
|
||||||
|
are ludic.base's baked form, read by hand (baked_tex.ludic: the "LBAK" header, the key and version); a
|
||||||
|
missing or stale one falls back to the PNG as before. `tex_load_dds_at` reads a .dds at an offset.
|
||||||
5
changes/bind-variable.md
Normal file
5
changes/bind-variable.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`bind Purse { money: g_money }` - a port member bound to a variable.** A member that takes nothing
|
||||||
|
may name a global instead of a function; the compiler writes the getter in the bind's file, so the
|
||||||
|
one-line wrapper is gone. A member that takes something is refused a variable.
|
||||||
7
changes/build-ir-in-temp.md
Normal file
7
changes/build-ir-in-temp.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`ludic build` keeps its LLVM IR out of the project.** The intermediate `.ll` was written beside
|
||||||
|
the binary (`build/<name>.ll`) and deleted after linking, so a project's tree held one for the
|
||||||
|
length of every build, and two builds at once deleted each other's - which surfaced as a
|
||||||
|
`clang: no such file` that read exactly like a compile error. It now goes to the run's own
|
||||||
|
temporary directory and goes with it. `--save-temps` still keeps it at `build/<name>.ll`.
|
||||||
9
changes/builtin-names.md
Normal file
9
changes/builtin-names.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**A function named like a built-in a call always takes is refused.** `function words(st, k)` compiled,
|
||||||
|
and every call to it became the built-in `words(n)` - n zeroed ints, with a pointer for n - and LLVM
|
||||||
|
refused the IR far from the cause. A top-level function whose name a call always takes as the
|
||||||
|
compiler's own (`words`, `keep`, `print`, `save`, `load`, `key`, ...; the table is
|
||||||
|
`selfhost/check/check_builtins.ludic`, held to `emit_call` by `ludic-dev syntax --check`) is now an
|
||||||
|
error at its declaration, as `run` already was. Every other built-in (`buffer`, `floats`, `double`,
|
||||||
|
...) yields to a function the program declares, in the checker as it already did in codegen.
|
||||||
7
changes/check-build.md
Normal file
7
changes/check-build.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic build --check` / `ludicc --check`: check without building.** The parse, the type checker
|
||||||
|
and the module rules (`export`, `uses`, layers, ports, registries) run, and nothing is emitted or
|
||||||
|
linked - about three seconds on Maroon Lake where a build takes about a minute. In this mode the
|
||||||
|
checker asks the module rules at each reference it resolves, since the emitter that usually asks
|
||||||
|
them does not run.
|
||||||
9
changes/check-stdin-file.md
Normal file
9
changes/check-stdin-file.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**Check an unsaved buffer.** `ludic build --check --diagnostics=json --stdin-file <path>` (and
|
||||||
|
`ludicc --check --stdin-file <path>`) checks the program as usual, but wherever the compiler would open
|
||||||
|
`<path>` - the entry, an import reached through a barrel, a component's `.xml` / `.lss`, an `.lres` -
|
||||||
|
it reads the text on stdin instead, so an editor's diagnostics follow typing without a save. Paths are
|
||||||
|
matched after normalising both (separators, relative to the working directory, `.` / `..` folded).
|
||||||
|
Diagnostics carry the file's usual name with lines and columns in the buffer; a `<path>` the program
|
||||||
|
never opens is reported as one warning.
|
||||||
7
changes/chunked-keys.md
Normal file
7
changes/chunked-keys.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**A chunked table's keys are unique across its map, and not interned.** A row's id is `(map, key)`, so a
|
||||||
|
tree moved into another chunk keeps it, and `ludicc --check` refuses a key written in two of a map's
|
||||||
|
chunk files, naming both. The keys are no longer interned: interning every key a player walked past would
|
||||||
|
have filled the bounded intern table and kept them all for good. A chunk slot keeps its keys in its own
|
||||||
|
buffers, rewritten in place when the slot is refilled; `intern(row.key)` keeps one past `_out`.
|
||||||
9
changes/contact-darkening.md
Normal file
9
changes/contact-darkening.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: patch
|
||||||
|
type: feat
|
||||||
|
Things sit ON the ground rather than hovering over it. The screen-space GI pass takes
|
||||||
|
a second, much tighter set of taps (a 0.40 m radius that grows with distance, with a
|
||||||
|
range check so a far surface behind a near one cannot darken it) and folds the result
|
||||||
|
into the ambient occlusion it already had. The wide radius answers "how enclosed is
|
||||||
|
this", which a trunk meeting grass barely registers; the tight one answers "is
|
||||||
|
something touching here", which is the shadow the eye looks for to place an object.
|
||||||
|
It is eight taps on a buffer the pass had already bound.
|
||||||
9
changes/declared-namespaces.md
Normal file
9
changes/declared-namespaces.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Namespaces are declared in Ludic.** `alias meth(labels) = target` in a `namespace` block makes
|
||||||
|
`Ns.meth(...)` a call to `target`, taking named arguments by those labels; with no label list the
|
||||||
|
target's own parameter names are the labels. The engine's 41 table-driven namespaces - `Http`,
|
||||||
|
`Udp`, `Process`, `Json`, `Value`, `Screen`, `Input`, `Audio`, `World`, `Tiled` and the rest, 438
|
||||||
|
methods - moved out of the compiler into `runtime/native/namespaces.ludic`, and a package owns an
|
||||||
|
API the same way. The code a program compiles to is unchanged byte for byte, and the checker now
|
||||||
|
checks an alias call's arguments against its target.
|
||||||
7
changes/def-from-resource.md
Normal file
7
changes/def-from-resource.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`def Recipes from "recipes.lres"` - a game fills a package's open registry from its own resource
|
||||||
|
file.** The entries are checked against the registry's record as the file is read, with errors at
|
||||||
|
the resource file's line, and they are defs of the module that wrote the line: the registry must be
|
||||||
|
open to it, and they sit in the stable order (the declaring module's entries, then other modules'
|
||||||
|
by name, and file order within a file).
|
||||||
134
changes/defaults-views-templates.md
Normal file
134
changes/defaults-views-templates.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Default parameters, components, views and templates.** A parameter can have a default (`pad: float = 8.0`).
|
||||||
|
A call leaves out what it does not change, and may pass its first arguments by position and the
|
||||||
|
rest by name.
|
||||||
|
|
||||||
|
A `view` declaration is the one bridge between a program and its UI. It names the fields a
|
||||||
|
template may read, the functions it may ask and the `on` events it may send, and it writes
|
||||||
|
`view_<name>() -> UiView`.
|
||||||
|
|
||||||
|
A `component Name { prop, state, fields, functions, on events }` declaration beside `Name.xml` and
|
||||||
|
`Name.lss` is a UI component. Its template and styles are compiled in (with `@import` inlined), each
|
||||||
|
mounted instance keeps its own props and state, styles are scoped to it, and a parent's `class`,
|
||||||
|
`style` and `id` land on its root.
|
||||||
|
|
||||||
|
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
|
||||||
|
with:
|
||||||
|
- `{expression}` bindings;
|
||||||
|
- `<if>`, `<else>` and `<each>`;
|
||||||
|
- props, `<slot/>` and per-instance `<state>`;
|
||||||
|
- `on-press` actions that send events, `set` state or `emit` to the component's user;
|
||||||
|
- component libraries (`export="true"`, `<import src as>`);
|
||||||
|
- HTML's elements (`div`, `p`, `h1`-`h6`, `ul`/`li`, `img`, `hr`, ...), with a default stylesheet;
|
||||||
|
- HTML's attributes: `id`, `class`, `style`, `hidden`, `disabled` and `onclick`, with any other
|
||||||
|
attribute kept for selectors;
|
||||||
|
- the CSS box model (padding and margin in 1-4 values, borders, `px` and `%`) and flex layout
|
||||||
|
(`flex-grow`, `justify-content`, `align-items`/`align-self`, `flex-wrap`, min and max sizes)
|
||||||
|
under CSS's property names;
|
||||||
|
- stylesheets, in a `<style>` or an `.lss` file (a Ludic StyleSheet) that others import and that
|
||||||
|
can `@import` more;
|
||||||
|
- CSS's selectors: `#id`, compound classes, `[attr=value]`, descendant and `>` combinators,
|
||||||
|
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
|
||||||
|
specificity.
|
||||||
|
|
||||||
|
- more CSS: custom properties and `var()`, `position` with insets and `z-index`, `em`/`rem`/`vw`/`vh`,
|
||||||
|
`@media`, wrapping text and ellipsis, `overflow`, `+`/`~`, `:nth-child(an+b)`, `:checked`, `:active`;
|
||||||
|
- more React: keyed lists, `<let>`, `<provide>` context, `<fragment>`, named slots, `on-mount` and
|
||||||
|
`on-unmount`;
|
||||||
|
- native elements a program draws itself (`ui_native`, `ui_fire`), and form controls;
|
||||||
|
- errors with file and line, hot reload (`ui_reload`), and an inspector-style dump.
|
||||||
|
|
||||||
|
The runtime is a UI framework, not only a template engine:
|
||||||
|
- it takes input itself: focus and keyboard navigation, the pointer, scroll boxes, `autofocus`;
|
||||||
|
- it has built-in controls (button, checkbox, radio, range, select, text, key), styled as CSS
|
||||||
|
parts;
|
||||||
|
- `ludic.ui/render3d.ludic` is a render3d backend, with textures, atlases, nine-slices, clipping
|
||||||
|
and scale;
|
||||||
|
- more CSS: `rgba()`/`#rrggbbaa`, `border-radius`, `outline`, `box-shadow`, `background-image`,
|
||||||
|
`border-image`, group `opacity`, `@keyframes` / `animation` / `transition`;
|
||||||
|
- HTML mixed content, and boolean attributes;
|
||||||
|
- `popover` (a top layer that keeps the pointer and keys, with light dismissal), `title` tooltips,
|
||||||
|
and `<progress>` / `<meter>`;
|
||||||
|
- importing `ludic.ui/render3d.ludic` installs the backend, and atlases take rows;
|
||||||
|
- hooks for the program's language, sounds and clock.
|
||||||
|
|
||||||
|
What a game's screens found missing, now in `ludic.ui`:
|
||||||
|
- `<input type="number" min max step>`: typed digits, Enter or leaving it commits them clamped, the
|
||||||
|
arrows step it;
|
||||||
|
- `<input type="key">` listens for any key (Tab and the arrows included) once Enter or a click starts
|
||||||
|
it; Esc stops it, Backspace clears it, `shown` names the value, and `ui_capturing()` tells the host;
|
||||||
|
- `note="..."` under any control's label (`.ui-note`); a range's `decimals`, `format="percent"` and
|
||||||
|
`unit`; a track laid out as a row, with the range's fill as tall as it;
|
||||||
|
- popovers anchored beside an element (`anchor="id"`, or a bare `anchor` for the element before it,
|
||||||
|
`placement`), flipped to the other side and kept on the screen;
|
||||||
|
- `flex-shrink` (a scroll box in a column takes the room its siblings leave), `flex: grow shrink`,
|
||||||
|
`order`, and text in a row wrapping in the room its siblings leave;
|
||||||
|
- `calc()` over px, %, em, rem, vw, vh and `var()`; `width: 0` and `height: 0` mean 0;
|
||||||
|
- `text-shadow`; tooltips of several lines; `ui_opacity()` for a native's draw;
|
||||||
|
- `border-image` drawn as painted with no background colour, tinted by one, and not at all under
|
||||||
|
`transparent`; a picture file drawn untinted (an atlas cell still takes `color`);
|
||||||
|
- the render3d backend loads a picture again when its file changes (`ui_image_reload`), draws a path
|
||||||
|
with a drive letter as a path, and slices a nine-slice by its texture's own width and height;
|
||||||
|
- a component root that is itself a component takes every user's class, style and id, and the
|
||||||
|
sheets that style it are weighed together by specificity;
|
||||||
|
- a component's event may be called `set`; a `string` prop given a number reads it as text; two
|
||||||
|
components of one name are an error naming both files;
|
||||||
|
- the scrollbar is `.ui-scrollbar` and `.ui-thumb`: a press on the thumb holds it where it was taken,
|
||||||
|
a press on the track jumps the thumb's middle there, and neither presses what is under the bar;
|
||||||
|
- a popover's own controls take its presses whatever lies under it, a press outside only closes it,
|
||||||
|
and while one is up the scroll boxes outside it do not take the pointer;
|
||||||
|
- the first gamepad moves the focus (d-pad, left stick), steps ranges and selects, and presses (A)
|
||||||
|
and goes back (B); a held direction, on the pad or the arrow keys, repeats after 0.42 s and then
|
||||||
|
every 0.11 s on the ui clock (`UiInput.held_*`, `pad_a`, `pad_b` for a host);
|
||||||
|
- pointer events: `on-pointerdown` / `pointermove` / `pointerup` / `drag` / `wheel` with `event.x`,
|
||||||
|
`y`, `dx`, `dy`, `button` and `wheel`, and `ui_native_input(tag, fn)` for a native; a press captures
|
||||||
|
the pointer until release; the pointer hits the topmost element in painting order, and
|
||||||
|
`pointer-events: none` lets it through;
|
||||||
|
- `on-down` and `on-up` on a button (the pointer, Enter or A), with `:active` true while it is held
|
||||||
|
there rather than whenever the pointer is down over it;
|
||||||
|
- an anchored popover's `align="start|center|end"`, and `within="id"` (by default the nearest
|
||||||
|
scroll box around it) for the bounds it is flipped against and kept inside;
|
||||||
|
- `text-fit: shrink MIN` shrinks a line to its box, then cuts it with an ellipsis; `line-height`;
|
||||||
|
an `em` reads the font size the element ends with (a `font-size` later in the rule, or in a later
|
||||||
|
rule), not the one it had so far;
|
||||||
|
- `min()`, `max()` and `clamp()`, in `calc()` or on their own; `top` / `right` / `bottom` / `left`
|
||||||
|
as a percentage or a `calc()` of one, of the containing block;
|
||||||
|
- a nine-slice's corners are clamped to half the box in each direction on its own and cut on whole
|
||||||
|
pixels (`ui_nine_cuts`), so a small key cap has no seam;
|
||||||
|
- `scroll-top="{px}"` holds a scroll box at an offset, with `on-scroll` when the player moves it;
|
||||||
|
`ui_scroll_set(id, px)` moves one once;
|
||||||
|
- `linear-gradient(...)` backgrounds; `aspect-ratio`; `object-fit` for pictures (the renderer's
|
||||||
|
`image_w` / `image_h`) and `ui_object_fit` for natives;
|
||||||
|
- `translate="no"` keeps an element's text as written; a title of several lines is translated whole,
|
||||||
|
else line by line;
|
||||||
|
- `<input type="key">` takes a mouse button (`UI_MOUSE_LEFT` / `RIGHT` / `MIDDLE`, 256-258) and is
|
||||||
|
`:capturing` while it listens;
|
||||||
|
- a `title` shows for the keyboard's focus too, after the same half second; a focus ring drawn
|
||||||
|
through a renderer with no `rect` no longer crashes;
|
||||||
|
- a component with no stylesheet of its own reads a theme's `:root` variables from around it (it
|
||||||
|
did; now a test says so);
|
||||||
|
- `ui_scale()` and `ui_box("id")`, the scale and an element's laid-out box, for a host;
|
||||||
|
- `on-submit` on a text field (Enter or A; the focus and text stay unless `clear-on-submit`);
|
||||||
|
- `zoom` on any element, and a length over a length in `calc()` is a plain number;
|
||||||
|
- `on-hold` every frame a button is held, with `event.dt` and `event.t`;
|
||||||
|
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
|
||||||
|
frame rate).
|
||||||
|
|
||||||
|
render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in
|
||||||
|
order (`mixed`).
|
||||||
|
|
||||||
|
A `view` field set to a literal or a named function's result needs no type.
|
||||||
|
|
||||||
|
Screens are drawn through a registered renderer. `Value` gains a float kind.
|
||||||
|
|
||||||
|
Also:
|
||||||
|
- A program's function named like one of the runtime's is refused; it had been silently taking the
|
||||||
|
runtime's own calls. So is one named like a compiler built-in (`run`, `exit`, `free`, `fill`,
|
||||||
|
...): every call to a program's own `run` compiled into C's `system()`, and clang failed on the IR.
|
||||||
|
- An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs`
|
||||||
|
read stale memory.
|
||||||
|
- A runtime error names the file its expression is in, not the program's.
|
||||||
|
|
||||||
|
Two declarations with one name (a package's private global and a program's, say) are reported as
|
||||||
|
such before type checking. They used to surface as a page of type errors about the wrong type.
|
||||||
6
changes/deps-alias-writes.md
Normal file
6
changes/deps-alias-writes.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
bump: patch
|
||||||
|
type: feature
|
||||||
|
**`ludic deps --writes` warns about a write through a local alias.** `let t = thing_cur` and then
|
||||||
|
`t.used = 1` writes another module's record just as `thing_cur.used = 1` does; a local bound straight
|
||||||
|
from another module's global (or from such a local) is now followed within its function and each
|
||||||
|
write through it listed as a warning. A reference that arrives from a function's result is not.
|
||||||
9
changes/deps-reach-32-bits.md
Normal file
9
changes/deps-reach-32-bits.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`ludic deps`: a reach counts every state apart, however the states are numbered.** The reach and
|
||||||
|
write-reach bitsets packed 60 states to a word, but an `int` is 32 bits, so `1 << 45` came back as bit
|
||||||
|
13 and states 32 apart shared a bit: a function taking both counted one, fewer than it takes, and the
|
||||||
|
counts (`widest_reach`, `widest_write_reach`, `--reach`, `--wreach`) rose and fell with how a program's
|
||||||
|
states happened to be numbered. The sets now hold 30 to a word. On Maroon Lake `widest_reach` goes
|
||||||
|
58 -> 76 and `widest_write_reach` 54 -> 64 - the real numbers, which the old count hid.
|
||||||
|
`examples/state/reach_wide.ludic` (40 states, S00 and S32 taken together) holds it.
|
||||||
10
changes/deps-reach.md
Normal file
10
changes/deps-reach.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic deps` sees through fn values, and lists the widest functions.** A step list or a registry of
|
||||||
|
fn values takes no state and still reaches every state its steps take; `widest_reach` is the most
|
||||||
|
states any function can come to - by a call, a `fn f` it writes, or a global holding fn values it
|
||||||
|
reads - reported beside `widest_function` with how many of them it does not take itself
|
||||||
|
(`the widest reach: app_boot (src/app/boot.ludic:30), 72 states (72 through calls and fn values it
|
||||||
|
does not take)`). `--widest N` lists the N functions that take the most states with what each
|
||||||
|
reaches; `--reach N` orders them by reach. A baseline written before this has no `widest_reach` and
|
||||||
|
does not hold it until it is rewritten.
|
||||||
7
changes/deps-write-reach.md
Normal file
7
changes/deps-write-reach.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic deps` says what a function can come to CHANGE** (`widest_write_reach`, and `--wreach N`
|
||||||
|
lists the functions by it): the states it reaches as `mut`, through calls, `fn` values and step
|
||||||
|
lists. Reach itself is sharper: `Port.member()` reaches that member's binding only, and
|
||||||
|
`Registry[i].field` (or a local holding `Registry[i]`) reaches that field only - a question asked of
|
||||||
|
a port or a table that also holds verbs no longer reaches the verbs.
|
||||||
7
changes/devlink-ui.md
Normal file
7
changes/devlink-ui.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic.devlink`: the interface's verbs.** A `DevlinkUi` port, every member defaulting to "not offered":
|
||||||
|
`ui_screen` (the screen's root class and its components), `ui_model "<Class>" "<out>"` and `ui_tree "<out>"`
|
||||||
|
(the game writes a component's model or the whole tree to a file, no `..`, and the answer names it, so a
|
||||||
|
datagram stays small) and `ui_override "<path>" "<file>"` (a template or stylesheet read from another file
|
||||||
|
and reloaded keeping state; `""` clears one, `"" ""` all). Answered at once; nothing made per frame.
|
||||||
9
changes/devlink.md
Normal file
9
changes/devlink.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic.devlink`: an editor's live link into a running dev build** (protocol v1, frozen with Ludic
|
||||||
|
Studio). Loopback UDP through the `DevlinkNet` port, one request a frame parsed in place from one fixed
|
||||||
|
8 KB buffer and answered into another; every verb a `DevlinkWorld` member defaulting to "not offered":
|
||||||
|
`ping`, `hello` (the build's schema hash as 16 hex digits), `cam_get` / `cam_set` / `cam_release`, `goto`,
|
||||||
|
`map_load`, `time`, `weather`, `shot`, `pause` / `resume` / `step`, the slow three answered later by id.
|
||||||
|
A socket is opened only when `enabled()` says so (a game binds `dev_tools`), a sender off this machine is
|
||||||
|
dropped, and a connected co-op session refuses everything but `ping` and `hello`.
|
||||||
8
changes/dilate-parallel.md
Normal file
8
changes/dilate-parallel.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: patch
|
||||||
|
type: performance
|
||||||
|
**Cut-out edge padding runs on every core.** `tex_dilate`'s passes hand their rows, sixteen at a time, to
|
||||||
|
`Job.parallel_for`: within a pass a row writes only its own still-masked texels and reads only
|
||||||
|
neighbours the mask already let go, so the bytes are the ones the single-threaded loop made. The worker
|
||||||
|
is handed plain buffers in a `DilateJob` and makes nothing. `tex_dilate_bytes` is the slice-taking
|
||||||
|
form (safe_api.ludic), and `examples/rendering/dilate.ludic` holds the result against the old loop
|
||||||
|
(prints DILATE OK). It was 206 ms of the main thread in a Maroon Lake boot.
|
||||||
10
changes/dispatch-kept-records.md
Normal file
10
changes/dispatch-kept-records.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**A dispatched action no longer allocates a record each time.** `dispatch A { ... }` made a fresh
|
||||||
|
record for the queue, and Ludic frees nothing, so a system dispatching every frame (an input's
|
||||||
|
`Move`, a frame's time) grew the program by a record a frame. The queue now keeps a list per
|
||||||
|
action: a dispatch takes the next one (making one only when all are queued), fills every field
|
||||||
|
as `new` would - given, or its default - and `drain_actions()` hands them all back once the queue
|
||||||
|
is empty. A reducer reads its action only during the drain, so nothing sees a record after it is
|
||||||
|
reused; keep what must last in the state, not the action. `ludic.base`'s `actions_test` holds a
|
||||||
|
reused record getting its defaults back.
|
||||||
8
changes/duplicate-definitions.md
Normal file
8
changes/duplicate-definitions.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**A name is defined once, for every kind of declaration.** Two functions with one name were
|
||||||
|
already an error; two `var`s or `const`s (or an enum and a const), or two `property` / `event`
|
||||||
|
records, kept the first definition silently. A game lost months to it: two files both said
|
||||||
|
`KEY_LEFT`, one meaning an arrow key's code and one a binding slot, and the menus read the slot.
|
||||||
|
They are now an error that names both files and lines. `examples/rejected/` holds the two cases,
|
||||||
|
checked by a new `reject_case` in the test runner (an example the compiler must refuse).
|
||||||
9
changes/ecs-growable-stores.md
Normal file
9
changes/ecs-growable-stores.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**The built-in ECS grows.** Every component was a fixed array of 1024 slots, so a game past 1024
|
||||||
|
entities could not have them (and until the last release silently corrupted memory trying). The
|
||||||
|
per-entity stores are heap blocks now, doubled by `L_grow` as entities outgrow them, the new slots
|
||||||
|
zero: 100 000 entities spawn and query. `Prop.has` bounds against the live capacity and
|
||||||
|
`Pool.capacity` answers it. A snapshot (`save`/`load`, `world_save`/`world_load`) records its slot
|
||||||
|
count first and a load grows to it before reading the stores back, so a snapshot's size follows the
|
||||||
|
world's instead of a fixed 1024. A mod's registered components grow with the rest.
|
||||||
31
changes/editor-schema.md
Normal file
31
changes/editor-schema.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**A schema for editors, and every error as JSON.** `ludicc --emit-schema out.json` (and `ludic
|
||||||
|
schema [file] [-o FILE]`) writes what the compiler resolved once the program type-checks: every
|
||||||
|
record with its fields' types, defaults, doc comments and places; every registry with its record,
|
||||||
|
prefix, resource file, openness and its entries in their final order after the open-registry merge
|
||||||
|
(key, constant, index, file:line:col of the entry and of each field value, and which file brought
|
||||||
|
which entries in); every const; and every function a `fn` value can name, with the `fn_type` a field sees (its states stripped).
|
||||||
|
Deterministic, `"schema_version": 1`. Fields and registries carry editor attributes on the existing
|
||||||
|
`@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
|
||||||
|
`@Color`, `@Node(field)`, `@Clip(field)`, `@Material(field)`, `@Tint(SLOT)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
|
||||||
|
a registry - which change nothing but go into the schema; `@Ref` naming no registry is an error, and
|
||||||
|
so is `@Node` / `@Clip` naming a field that is not a glTF (`@Asset("gltf")`, or an `@Ref` to one), and
|
||||||
|
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check
|
||||||
|
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
|
||||||
|
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.
|
||||||
|
|
||||||
|
`Build.schema_hash()` answers FNV-1a 64 of the program's own schema (the bytes `ludic schema` prints),
|
||||||
|
computed only when a program names it, and 0 under `ludicc --release`, which `ludic bundle` now passes.
|
||||||
|
|
||||||
|
A target no part of the program declares (an `@Ref` registry, an `@Tint` or listed `@OneOf` constant) is
|
||||||
|
a warning and `"unresolved": true` in the schema, so a package can name the game's registry; a name of
|
||||||
|
another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
|
||||||
|
checked against them.
|
||||||
|
|
||||||
|
The schema has a `components` list: each UI component's module, place, doc, template and stylesheet
|
||||||
|
paths, its `props` and `state` (type, default as written, place, doc), `states_read` (the states its
|
||||||
|
header names, apart from its model), `derived` fields with their types, and the `functions` and
|
||||||
|
`events` its template calls with their parameters (states and instance stripped), and the
|
||||||
|
registered native tags its template uses. A `natives` list gives every `ui_native` /
|
||||||
|
`ui_native_input` call with a literal tag: the tag, the function called, its handler and its place.
|
||||||
6
changes/engine-alias-shadow.md
Normal file
6
changes/engine-alias-shadow.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**A function named like an engine namespace method's target is refused where that method is
|
||||||
|
called.** `Random.range` is `rng_range`, so a package's own `rng_range(a, b, c)` silently took
|
||||||
|
every `Random.range(1, 6)` (and the checker then asked for its third argument). It is now an error
|
||||||
|
naming the function, the namespace method and the call.
|
||||||
5
changes/expect-eq-strings.md
Normal file
5
changes/expect-eq-strings.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`expect_eq` on strings compares their text.** It lowered to an integer compare of two pointers,
|
||||||
|
which the IR refused; now two strings with the same text are equal (a null only to a null), and a
|
||||||
|
failure prints both: `expect_eq failed (got "camp", want "lake")`.
|
||||||
5
changes/expect-floats.md
Normal file
5
changes/expect-floats.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`expect_eq` and `expect_near` take floats.** On a float or a double they compared with an integer
|
||||||
|
instruction, and the build failed in clang ("defined with type 'float' but expected 'i32'"); they
|
||||||
|
compare as floats now (the wider kind of the two) and a failure prints the numbers.
|
||||||
12
changes/fmt-editor-output.md
Normal file
12
changes/fmt-editor-output.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic fmt` for editors.** `ludic fmt --lint --json` prints the violations `--lint` reports as one JSON
|
||||||
|
array on stdout, `[{"file", "line", "col", "rule", "message"}]` ordered by file, line and column (the
|
||||||
|
summary on stderr, `--lint`'s exit status, and the baseline never rewritten). `ludic fmt -` formats
|
||||||
|
stdin to stdout under the project found from the working directory (the nearest `package.ludic`
|
||||||
|
upwards), and refuses a buffer that does not read as Ludic - an open string, a bracket never closed or
|
||||||
|
closed by the wrong one - with exit 2 and `<name>:<line>:<col>: error: ...` on stderr.
|
||||||
|
`--stdin-name <path>` makes the buffer that file: the project is found from its directory, and
|
||||||
|
`ludic fmt - --lint --json --stdin-name <path>` judges it against that file's baseline and `lint
|
||||||
|
paths`, reporting it under the name given. Hooks around `fmt` and `get` read nothing from stdin and
|
||||||
|
write to stderr when the command's stdout is a program's (`--json`, `-`).
|
||||||
5
changes/friend-of.md
Normal file
5
changes/friend-of.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`friend module lab of fishing, data` - a friend of some modules, not all.** A scoped friend sees
|
||||||
|
the private names of the modules it names and only the exports of every other; `friend module lab`
|
||||||
|
alone still sees everything.
|
||||||
9
changes/function-values.md
Normal file
9
changes/function-values.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**Functions are values (L2).** `fn(int, float) -> bool` is a type, `fn name` is any top-level
|
||||||
|
function's value (it used to be only a thread worker's address), and a call through a local, a
|
||||||
|
global, a record field, a slice element, a parameter or a result of a function type is an
|
||||||
|
indirect call. Two different function types do not mix, a call through one checks its argument
|
||||||
|
count, and a value may be `null`. A registry can hold behaviour and a package can take
|
||||||
|
callbacks. `Job.parallel_for` still checks that its worker takes (int, pointer) and returns
|
||||||
|
nothing. `ludic-dev selfhost-build` now says why it failed instead of exiting 1 silently.
|
||||||
5
changes/gb-window-fill.md
Normal file
5
changes/gb-window-fill.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bump: patch
|
||||||
|
type: feature
|
||||||
|
**The blades' density window can be filled without a GPU.** grass_density.ludic's gb_frame is split: gb_window_fill
|
||||||
|
fills the GB_TILES x GB_TILES window of density tiles round the camera's (zeros off the map) and sets its corner,
|
||||||
|
and gb_frame sends it. The same bytes as before; a test reads gb_win after gb_window_fill.
|
||||||
8
changes/generics.md
Normal file
8
changes/generics.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Generic records and functions.** `property Pool<T> { items: []T }`, `function first<T>(xs: []T)
|
||||||
|
-> T` and `function map<T, U>(xs: []T, f: fn(T) -> U) -> []U`; a type writes an instance as
|
||||||
|
`Pool<Thing>`, nested as deep as needed. A call's type arguments come from its arguments, or from
|
||||||
|
the declared type its result is written into, and are refused with the parameter named when
|
||||||
|
neither says. Each instance is compiled once as an ordinary record or function. `ludic-fmt` keeps
|
||||||
|
`Pool<Thing>` together while still spacing `a < b`.
|
||||||
13
changes/ground-painted-zones.md
Normal file
13
changes/ground-painted-zones.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic.render3d`: painted ground layers grow solid things, with ids, and the trample is data.**
|
||||||
|
`ground_fill`'s candidate is its own function, `ground_candidate` (pure `gf_*` steps with the density read
|
||||||
|
between them, into a caller-held `GroundCand`), and `ground_fill` draws exactly its answers - the same
|
||||||
|
operations in the same order as before, so every cover layer grows bit for bit what it did. A layer with
|
||||||
|
`solid: true` is filled at `step0` with band 0's hashes whatever the camera, a far band drawing a stable
|
||||||
|
subset; each thing has an int id from (layer, chunk, cell) (`ground_solid_id`), and `ground_solid_list` /
|
||||||
|
`ground_solid_at` answer a chunk's things or one by id for physics, the nav bake and saves.
|
||||||
|
`r3d_ground_clearing(x, z, r_in, r_out, floor)` hands the trample over as discs the editor can see, beside
|
||||||
|
the `r3d_on_ground_trample` callback, which still works. `tests/ground_fill_test.ludic` holds all of it to
|
||||||
|
`tests/ground_fill_golden.json` (written by `tests/gen/ground_fill_golden.ludic`), the file the studio's
|
||||||
|
TypeScript generator is tested against.
|
||||||
6
changes/http-text-copy.md
Normal file
6
changes/http-text-copy.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**`Http.text` and `Http.header` return copies.** They handed back the handle's own buffer (and on macOS the
|
||||||
|
response object's string), which `Http.free` then released: a text read before the free and used after it
|
||||||
|
was garbage or empty - maroon-lake's map list wrote a 0-byte maps.json. Each call now returns a string that
|
||||||
|
is the caller's to keep. Read a body once per response.
|
||||||
8
changes/i18n-error-mode.md
Normal file
8
changes/i18n-error-mode.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**English left is an error (phase 26.9).** Under a `lang` line, a template's own words, a text
|
||||||
|
attribute's, a quoted choice that reads as words and a `@Text` row still holding English now refuse
|
||||||
|
the build, where they were warnings; `ludic deps` still counts them as `english_left`. Hole counts and
|
||||||
|
undescribed splits stay warnings. ludic.ui's own words - the key field's "Right click", "Middle
|
||||||
|
click", "Left click" and "press a key..." - are keys (`ui_tk(ui_st, k"ui.right_click", plain)`,
|
||||||
|
`ui.*` in the program's `.po`), with their plain English for a program that binds no translator.
|
||||||
9
changes/i18n-fill-nested.md
Normal file
9
changes/i18n-fill-nested.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**Text keys below a row, padding, and `tr`'s cast.** A `@Text Key` in a record nested in a registry
|
||||||
|
row (and in each item of a list of them) is filled with its derived key, `<registry>.<row>.<field>.<i>.<field>`,
|
||||||
|
as a top-level one is, and a `@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as
|
||||||
|
the source `.po` has - so no `.lres` spells a key. `field: null` is no text. `trf` / `trn`'s trailing
|
||||||
|
`""` arguments are padding and not counted against the English's holes. And `string(x)` of a string or
|
||||||
|
a `Key` is no allocation to the escape analysis: it is `x` itself, so a `tr(key)` that returns it
|
||||||
|
passes `arena strict` (a template's lone hole still copies).
|
||||||
9
changes/i18n-keys-only.md
Normal file
9
changes/i18n-keys-only.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: change
|
||||||
|
**ludic.i18n draws plain text as it is: the English path is gone (phase 26.9).** `L` makes a key, a
|
||||||
|
key glued into text, or a line bracketed inside another; anything else - a player's name, a chat
|
||||||
|
line, a number - is drawn as it is, in every language, so a player named "Settings" stays
|
||||||
|
"Settings". Removed with it: the lookup of English words (exact lines, patterns with holes, a
|
||||||
|
paragraph a sentence at a time, padding), `Ln` (use `trn(kn"...")`), `i18n_pattern_count`, and an
|
||||||
|
English argument's own lookup inside a key's hole. A language `.po` is read for its keys and
|
||||||
|
plurals only. A game still on English msgids draws them untranslated until they are keys.
|
||||||
10
changes/i18n-keys.md
Normal file
10
changes/i18n-keys.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic.i18n`: keys (phase 26).** A key names what a text is for, and `en.po` says it in English like any
|
||||||
|
other language. A key is a string with a marker byte (`I18N_KEY`, `I18N_PLURAL`), its arguments after
|
||||||
|
byte 31, so the code that makes text never takes `I18nState`: `tr(k)`, `trf(k, a, b, c, d)` and
|
||||||
|
`trn(k, n, a, b, c)` build it, and `L` makes it into text where it is drawn - the language in use, else
|
||||||
|
en.po (read the first time a key is asked for), else the key itself, `[[key]]` in a developer's build
|
||||||
|
(`i18n_loud`). Holes take their arguments in the language's order, a key argument made first; plural
|
||||||
|
keys go by each language's rule. A string with no marker takes the old English path, so a game can
|
||||||
|
move over a file at a time.
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue