Compare commits

...

211 Commits

Author SHA1 Message Date
75890b1a71 /apps/tools/twndel: thingies 2025-03-15 07:59:55 +03:00
73db3e57dc various small tweaks 2025-03-13 03:08:35 +03:00
2975aa2dfb comparing with whole number floats is okay. 2025-03-13 02:54:40 +03:00
6726faf719 twn_textures: lock cache when adding requests 2025-03-13 01:18:07 +03:00
183dfa6be5 twn_lines: combine differently colored lines into same batch 2025-03-12 12:00:53 +03:00
e974194af0 /docs/wiki: G1. Trigonometry starter 2025-03-12 01:36:42 +03:00
8607aa48ec /apps/tools/twndel: face hovering 2025-03-11 07:40:18 +03:00
f6600dfbda norm is needed after all. 2025-03-11 07:09:55 +03:00
bdabd04388 /apps/tools/twndel: reversable triangulation, fix to point finding with triangles 2025-03-11 07:02:52 +03:00
0e075ec334 /apps/tools/twndel: texture painting, face selection 2025-03-11 04:58:44 +03:00
b256fc903a /apps/tools/twndel: triangluation, triangle render 2025-03-11 02:41:03 +03:00
b52ecaeaa0 remove logging 2025-03-11 01:54:36 +03:00
37e46e9a7e /apps/tools/twndel: initial texture projection work 2025-03-11 01:53:45 +03:00
a472e6af52 /apps/demos/scenery: simpler normal_at(), clean one bit 2025-03-11 01:28:08 +03:00
66b2f04d9d fix missing texture sizing, fix bordered repeated texture upload, make quads default to repeating for now 2025-03-10 22:10:53 +03:00
90f4097070 /apps/templates/zig: depend on /src/*.zig sources for rebuilding, add .gitignore 2025-03-10 10:11:10 +03:00
829ff4780c zig template 2025-03-10 09:58:28 +03:00
8e15c9ec3c twn_util: dont add empty list 2025-03-10 07:05:45 +03:00
474ea84a77 /apps/tools/twndel: some data 2025-03-10 06:54:23 +03:00
7b8b9416ba twn_util: file_read() :images 2025-03-10 06:54:10 +03:00
8ed8158ae6 twn_util: file_read() with :exists 2025-03-10 05:34:29 +03:00
48e3a4c233 /share/twn_api.json: add new logging funcs 2025-03-10 05:29:56 +03:00
56530f9864 twn_util: final cleaning up, introducton of powerful file_read() 2025-03-10 05:19:58 +03:00
f86f3dd41a /apps/tools/twndel: use roundf for snapping 2025-03-09 08:12:29 +03:00
adae6be7e5 /apps/tools/twndel: camera translation 2025-03-09 07:38:32 +03:00
cd3033f9c4 /apps/tools/twndel: add reference lines 2025-03-09 06:53:54 +03:00
e11e63f273 /apps/tools/twndel: more plane awareness 2025-03-09 06:24:05 +03:00
75737b738f /apps/tools/twndel: plane intersection based point move 2025-03-09 00:30:20 +03:00
ce2c2513aa /apps/tool/twndel: grab.png 2025-03-08 21:57:53 +03:00
36c0af9953 hopefully more portable way of packaging binary embeds 2025-03-08 18:11:39 +03:00
826622cd58 twn_draw: proper ortho unproject 2025-03-08 04:38:56 +03:00
78b6a26de9 twndel: fix y axis point move 2025-03-08 04:10:14 +03:00
5f7b8bac6d undo, axis editing 2025-03-08 02:20:31 +03:00
6d6230c6a1 twn_audio: fix ogg vorbis sample reuse 2025-03-08 01:38:38 +03:00
c07e16490e model tool progress: initial selection 2025-03-08 00:50:47 +03:00
f5e55bb997 /apps/tools/twndel: point selection, sounds and other assets 2025-03-07 20:56:19 +03:00
1e6e323fe1 twn_draw: remove logging 2025-03-07 20:21:36 +03:00
dbf9599fe5 twn_api.json: add draw_camera_unproject() 2025-03-07 19:40:56 +03:00
923cd81571 twn_draw: draw_camera_unproject() 2025-03-07 19:31:46 +03:00
733a1786ab twn_draw: multiply zoom by 0.1 to match projections 2025-03-07 11:16:31 +03:00
a03e1d885d twn_camera: fix aspect of ortho projection 2025-03-07 11:07:05 +03:00
67feb5974a /apps/tools/twndel: start the thing 2025-03-07 07:10:44 +03:00
5be4ed4645 twn_lines: bail on empty batch 2025-03-07 07:05:20 +03:00
4a41f47a58 twn_textures: fix freeing of missed texture 2025-03-07 06:19:44 +03:00
35bb26705a twn_text: use full ascii range 2025-03-07 05:16:46 +03:00
13bc71a28d twn_text: embed default font 2025-03-07 03:35:35 +03:00
b97a155de4 /bin: make utilities quit on error 2025-03-07 02:56:12 +03:00
5df80addeb /apps/demos/scenery: no drifting :( 2025-03-07 00:51:07 +03:00
787977b747 /apps/demos/scenery: drift ! steer ! 2025-03-05 05:26:05 +03:00
f90b973d86 /apps/demos/scenery: more than almost 2025-03-04 09:45:49 +03:00
32675c012c /apps/demos/scenery: ...almost...🚬 2025-03-04 03:30:01 +03:00
a97515e948 twn_textures.c: fix atlas packing, allow out of order population 2025-03-03 00:57:19 +03:00
ed8e826b94 /apps/demos/scenery: something... 2025-03-02 23:19:27 +03:00
4e5ff9433c /apps/demos/scenery: box is almost coherent... 2025-03-02 03:37:02 +03:00
55829a1bef /apps/demos/scenery: fix world origin relation 2025-03-02 01:04:09 +03:00
119bd52c51 /apps/demos/scenery: some friction 2025-03-02 00:02:32 +03:00
5abd1ced1c /apps/demos/scenery: vehicle... 2025-03-01 16:42:14 +03:00
80db96672d stb_ds.h: fix implicit casts to int, resulting in bogus bitshift 2025-03-01 12:15:41 +03:00
2f6f7852be twn_api.json: add draw_line_3d() 2025-03-01 03:59:55 +03:00
307d5552f6 twn_lines.c: 3d case 2025-03-01 03:46:11 +03:00
5911cbd980 take care of warnings 2025-03-01 01:06:32 +03:00
e47b761a2c twn_lines.c: introduction with proper impl 2025-02-28 23:50:12 +03:00
844283c2fb /apps/examples/cirlce-raster: update 2025-02-28 17:35:58 +03:00
09eac707c3 draw: use GLint in circle element buffer 2025-02-28 17:27:01 +03:00
5e89710458 rename twn_engine_api.h to twn_api.h 2025-02-28 16:42:33 +03:00
4bc1feb826 /apps/demos/scenery: fix ramp bug, increase gravity 2025-02-26 23:19:22 +03:00
1c3973c6a2 /apps/demos/scenery: narrower cull, new assets used 2025-02-26 19:57:38 +03:00
da5bdb4fae /apps/demos/scenery: increase walking speed a bit 2025-02-26 17:22:02 +03:00
ed2afec5a7 /apps/demos/scenery: culling 2025-02-26 17:08:45 +03:00
6812c7c13d add trees to scenery, disable mipmapping by default, increase index buffer size again 2025-02-26 16:17:44 +03:00
8c0f43ec34 draw: draw_distance for 3d spaces, proper positioning of skybox according to it, scenery demo on circle rasters 2025-02-26 15:53:59 +03:00
23fbd45564 increase quad element buffer size 2025-02-26 13:29:28 +03:00
a36459397e draw: increase far Z, separate path for space quads, fix billboard batching 2025-02-26 13:27:09 +03:00
5f3920fdba /apps/demos/scenery: use quads 2025-02-26 11:28:59 +03:00
f57525cea6 /apps/demos/scenery: remove title scene 2025-02-26 11:26:38 +03:00
6b2901be28 /apps/demos/crawl: cleanup, document 2025-02-25 22:20:35 +03:00
9f0d15b9f6 /apps/twnlua: add ---@meta annotation 2025-02-23 22:07:32 +03:00
b46331e08d no c brain 2025-02-23 17:34:00 +03:00
d2938da8e2 /apps/demos/crawl: dont scale by 2 2025-02-23 17:28:45 +03:00
9134e51817 /apps/demos/crawl: add LICENSES 2025-02-23 17:19:33 +03:00
d66eda1894 add texture border to atlas residents, actually make use of mipmapping 2025-02-23 17:14:05 +03:00
a88392b9e9 /docs/wiki: T1.4 Developing 2025-02-22 21:21:42 +03:00
05f85062e8 fix command lists 2025-02-22 20:50:38 +03:00
d5aec5e6e1 /bin/twn: devcompl command to generate clangd completions in root 2025-02-22 20:29:28 +03:00
62866d33ae add notes explaining emscripten considerations 2025-02-22 16:19:22 +03:00
ce7240d423 dont generate source maps for web as it crashes lua build 2025-02-22 01:39:04 +03:00
7a38f7bcf3 /bin/twnbuild: fix cmake cache for web target 2025-02-22 01:32:55 +03:00
affaf7f557 cleanup templates 2025-02-22 01:14:20 +03:00
a223506a5f /bin/twn: support run command for --target=web 2025-02-22 00:39:24 +03:00
98d7d76a42 twn.png 2025-02-21 23:34:12 +03:00
814269ab0c textures working on web, separation of vertex and index buffers (actually matters) 2025-02-21 23:34:01 +03:00
d76ea06470 poll workers for non-threaded support, emscripten main loop, remove twn_model.c 2025-02-21 21:16:15 +03:00
dc6b298532 proper-er vector op flags 2025-02-21 19:37:22 +03:00
f25e27b102 changes to build system, emscipten progress (can render solid color, yippie!) 2025-02-21 19:06:19 +03:00
dd4fc45be3 attempt to build web version out of emscripten legacy gl wrapper 2025-02-21 18:07:04 +03:00
85e47dd677 api return restriction 2025-02-21 12:48:51 +03:00
a020b92824 /apps/demos/crawl: facing textures 2025-02-21 02:00:00 +03:00
b6347996f9 /apps/demos/crawl: new tile texture, solid walls 2025-02-21 01:36:46 +03:00
66c525a0d4 /apps/demos/crawl: visuals and stuff 2025-02-21 01:13:53 +03:00
70fab28158 /apps/twnlua: catch self-include stack busting 2025-02-21 00:18:22 +03:00
80a4ae3d0e progress on /apps/demos/crawl 2025-02-20 22:20:02 +03:00
bd3b090f6f add /apps/demos/crawl 2025-02-20 20:10:37 +03:00
6eb0730c52 mark identity parameter in log_ functions optional 2025-02-20 19:52:14 +03:00
a231d650f2 twn_util.c: add file_read() 2025-02-20 19:51:52 +03:00
e15975bfaa update to lua template 2025-02-20 17:25:11 +03:00
b67bc92857 remove optional by pointer texture_region parameters 2025-02-20 16:19:03 +03:00
991196f7c8 update to docs 2025-02-20 16:03:50 +03:00
0b89c90ad7 move /docs/interop.md to about-townengine.html 2025-02-20 15:42:05 +03:00
3d51c8c48f update about-townengine.html 2025-02-20 14:29:30 +03:00
21d8e2c5a5 actually set the interval 2025-02-20 14:06:18 +03:00
f805bf3f92 add interval from timer_elapse_seconds() result, add timers to twn_api.json 2025-02-20 14:04:04 +03:00
5228fa7e41 update platformer demo 2025-02-20 13:54:42 +03:00
f044a75ffe reorganization of twn_util.h, reletion of some seldom used procedures 2025-02-20 13:48:44 +03:00
723ccf1126 combine twn_texture_modes.h into twn_textures_c.h 2025-02-20 13:05:17 +03:00
6bd3afe9b2 move and combine option macro headers from public interface 2025-02-20 13:01:02 +03:00
d90bf4cbe2 twn_audio.c: fix freeing on unnamed channels 2025-02-19 21:17:13 +03:00
48f34f4623 twn_audio.c: fix unnamed channel audio 2025-02-19 21:13:21 +03:00
9c007f34df remove /docs/packaging.txt 2025-02-17 21:16:53 +03:00
a1f4599efd twn_text.c: dont segfault on font not found 2025-02-17 12:32:01 +03:00
4c1a8e087a /bin/twn: pass --debug switch to gdb command 2025-02-17 12:31:33 +03:00
a2b1f1820a /bin/twnbuild: more options 2025-02-17 12:30:17 +03:00
85ec8d3366 make unified build work on windows 2025-02-17 11:08:38 +03:00
7eebc7a2d7 minilua.h: revert changes, realized that lua_newstate is public 2025-02-17 10:57:31 +03:00
2b26fad983 twn_textures.c: fix error in deinit 2025-02-17 10:50:50 +03:00
47799deb8b /apps/twnlua: fix for windows, parametrize newstate for alloc func 2025-02-17 10:39:10 +03:00
1cd4bfa638 twn wiki on windows 2025-02-17 10:01:48 +03:00
9beef7686e bug suggest 2025-02-16 01:40:17 +03:00
cee344c7c1 /docs/wiki/packaging.html 2025-02-16 01:31:42 +03:00
18a76649b9 twn: fix symlinks but fr 2025-02-16 01:06:27 +03:00
88a4876d91 twn: fix symlinks 2025-02-16 00:46:15 +03:00
835edd737c Merge pull request 'twn: add twn to project on twn init' (#2) from yagi/townengine:twn-init-symlink-twn into main
Reviewed-on: http://tochie.space:3001/veclavtalica/townengine/pulls/2
2025-02-16 00:15:54 +03:00
9a486fa912 twn: add twn to project on twn init 2025-02-16 00:13:44 +03:00
d4ce6ab9ec add license file to tomlc99 dist 2025-02-16 00:11:18 +03:00
24b417c287 twn_loop.c: watch pack changes from pack dependencies 2025-02-15 23:37:57 +03:00
5a83381ae1 twn_draw: texture_region option for draw_billboard() 2025-02-15 22:42:14 +03:00
793bd850f6 /apps/twnlua: ctx.udata preservation in reload, no export in .so, ignore /data/scripts/twnapi.lua 2025-02-15 22:19:14 +03:00
29d163216c --no-sanity-timer option and its use in twn gdb 2025-02-15 15:09:01 +03:00
ffc3badc50 /apps/twnlua: output lua_loadbuffer errors 2025-02-14 21:52:06 +03:00
bedfe0cdfb fix twnlua :deadinside: 2025-02-14 21:07:59 +03:00
927f284fda Merge branch 'yagi-disable-luaserver-diag' 2025-02-14 20:45:29 +03:00
2616549f88 disable lua server diagnostics in twnapi.lua 2025-02-14 20:43:11 +03:00
cc4f7f7417 /apps/twnlua/docgen.py: remove Control legacy 2025-02-14 20:38:09 +03:00
f81c583319 push as is, half baked broken twn_models.c 2025-02-14 19:51:34 +03:00
0df0a9226f optimized twnlua building (10.5s -> 8.5s) 2025-02-10 18:29:35 +03:00
af1b9caedc twn_audio.c: use SDL2/SDL.h directly in hopes that its used precompiled 2025-02-10 18:23:05 +03:00
72d1941091 make libxm compiled as a single translation unit 2025-02-10 17:52:19 +03:00
8a58336d16 some flag play 🚬 2025-02-10 15:41:37 +03:00
1818532ec9 reuse draw_camera() in axial variant 2025-02-10 14:42:30 +03:00
e9f8dbebbf refactor toml reading a bit 2025-02-10 14:36:57 +03:00
c81f95e571 remove redundant for us opengl loading code from glad.c 2025-02-10 14:05:19 +03:00
5ba11dc584 remove texture upload profiling 2025-02-09 08:30:46 +03:00
f2aded9046 twn_dynamic_game_object.c: only reload on creation event 2025-02-09 08:20:39 +03:00
d6aaef3f68 assume constant dimensions for created textures, shaves a lot of time in uploading 2025-02-09 08:07:58 +03:00
322fbf6bbd twn_textures.c: don't zero fill intermediate surfaces 2025-02-09 07:51:21 +03:00
5a7d7433d1 wip multithreaded texture load 2025-02-09 07:35:27 +03:00
037548436d remove ; from styles in wiki 2025-02-09 07:32:53 +03:00
5e27845e55 ignore pre c11 compat 2025-02-09 07:32:33 +03:00
3bf8d7bedb /apps/twnlua: actually use 32 bit 2025-02-08 12:47:10 +03:00
85d7d54eed twn_loop.c: quit subsystems 2025-02-08 00:22:34 +03:00
8c248cb3fb twn_models.c: ignore deinit if it's not needed 2025-02-08 00:19:22 +03:00
145b040a0f /docs/wiki: add {twn} plaque 2025-02-07 19:29:27 +03:00
559ff9fedc proper state, handling of fans 2025-02-07 13:42:01 +03:00
990135105a fix missing texture over NULL 2025-02-07 12:47:40 +03:00
7040d6f218 wip model loading + workers 2025-02-07 10:19:36 +03:00
cb88b4bcc5 delay subsystems, detach opengl load thread, post background color on init 2025-02-05 03:31:07 +03:00
8110789b3a add --as-needed 2025-02-05 02:51:52 +03:00
0eadeb7e9d /bin/twn: propagate args to build instead of exe in run 2025-02-05 02:27:16 +03:00
3d10e1782a /apps/twnlua: fix dest folder for docgen 2025-02-05 01:25:30 +03:00
d9df3f9b04 twn_draw.c: draw_camera_2d()! 2025-02-05 00:54:38 +03:00
d9d7072c86 add control strings to wiki 2025-02-05 00:09:34 +03:00
3733b53cc5 twn_utils.c: fix profile command type 2025-02-04 09:07:31 +03:00
b6b436e1b7 /apps/twnlua: don't compile stb_ds.h 2025-02-04 09:05:29 +03:00
02b5ac4cc3 input system rework 2025-02-04 07:32:25 +03:00
4efe80bb5a /bin/twn: add init command to copy templates over 2025-02-04 06:01:41 +03:00
f3a2dc9063 stuffs to wiki 2025-02-04 01:13:38 +03:00
53c43a8f34 various SDL hints to try out ! 2025-02-04 01:04:34 +03:00
507bff6ed8 /docs/wiki: add clause of low latency 2025-02-04 00:31:15 +03:00
00636d65a9 fix resizing events, clean up the code 2025-02-04 00:24:31 +03:00
42253fc58a fix artefacts 2025-02-04 00:24:01 +03:00
c6cbf941a2 introduce audio_model = "push" to twn.toml 2025-02-04 00:21:30 +03:00
277d1b2e10 /bin/twn: add wiki command 2025-02-03 22:31:53 +03:00
46955a19c1 oopsie 2025-02-03 22:28:29 +03:00
9ab3e942cd /bin/twnbuild: shortening 2025-02-03 22:26:51 +03:00
c7bb317ead /docs/wiki: fix links, add 1.2 Wiki 2025-02-03 22:15:46 +03:00
241e72be1a /docs/wiki/style.css: add bottom padding to the page 2025-02-03 22:11:10 +03:00
f4fccc08c4 start of the /docs/wiki 2025-02-03 21:55:26 +03:00
2286cdefeb ignore docgen artifact in lua template 2025-02-02 23:11:11 +03:00
00ada15dbc remove unneeded cmake file from lua template 2025-02-02 23:10:47 +03:00
96b6b7e70b /bin/twnbuild: python based build solution 2025-02-02 23:08:02 +03:00
ccfdfd8a35 half pi. 2025-02-02 05:39:47 +03:00
7284bb726a make yaw = 0 result in (0, 0, 1) target vector 2025-02-02 05:32:18 +03:00
87b33c2e0c /apps/twnlua: replace dots to forward slashes in module lookup 2025-02-02 05:00:54 +03:00
732a3579b0 /apps/templates/lua: add .gitignore 2025-02-02 04:27:44 +03:00
2f629433aa catch null font, report it as unimplemented 2025-02-02 04:12:56 +03:00
6d58e964bc use ccalloc in input bindings 2025-02-02 03:15:05 +03:00
0014458dbb actuall no spam. i swear. 2025-02-02 03:13:58 +03:00
9112630330 don't report useless opengl messages 2025-02-02 02:17:37 +03:00
108810d68a /apps/twnlua: propagate errors in physfs_loader() 2025-02-02 02:10:05 +03:00
2c94efb796 /apps/twnlua: add error on attempt to import twnapi.lua 2025-02-02 01:59:27 +03:00
11ec35bc8a /apps/twnlua: add returns in docgen.py 2025-02-02 01:53:03 +03:00
2120f6876c camera reset and default state 2025-02-02 01:41:02 +03:00
dfd888a80a don't bypass x11 compositor 2025-02-02 00:50:26 +03:00
695784301d twn_filewatch.c: typo 2025-02-01 14:25:54 +03:00
98d19495a2 another ccaloc moment 2025-02-01 14:00:18 +03:00
0929aa9be4 move shell html to /src/ 2025-02-01 13:57:51 +03:00
898c11bbdf /share/twn_api.json: add draw_line 2025-02-01 13:47:35 +03:00
aeabb17f86 create /apps/temapltes/ structure, clean up cmake files to use directory name for binary name directly 2025-02-01 13:47:17 +03:00
24e8dc052d fixes 2025-01-31 05:11:10 +03:00
3f264ca0ad remove busted free 2025-01-31 02:52:33 +03:00
a7557fceb4 Merge remote-tracking branch 'origin/main' 2025-01-31 02:49:33 +03:00
2e29cfcfe2 dmon license 2025-01-30 04:33:24 +03:00
185 changed files with 9671 additions and 4819 deletions

4
.gitignore vendored
View File

@ -2,13 +2,17 @@
* *
!*.* !*.*
!*/ !*/
!bin/*
!Makefile !Makefile
!LICENSE
!COPYING
**/*.exe **/*.exe
**/*.html **/*.html
**/*.js **/*.js
**/*.wasm **/*.wasm
**/*.wasm.map **/*.wasm.map
**/*.data
**/*.so **/*.so
**/*.dll **/*.dll
**/*.7z **/*.7z

View File

@ -5,6 +5,8 @@ project(townengine LANGUAGES C)
set(CMAKE_MESSAGE_LOG_LEVEL "WARNING") set(CMAKE_MESSAGE_LOG_LEVEL "WARNING")
set(CMAKE_INSTALL_MESSAGE NEVER) set(CMAKE_INSTALL_MESSAGE NEVER)
# TODO: test whether webgl 1 is good enough.
# SDL dependencies # SDL dependencies
# for whatever reason Emscripten has SDL2 config, but not actual SDL2 port by default # for whatever reason Emscripten has SDL2 config, but not actual SDL2 port by default
if(NOT EMSCRIPTEN) if(NOT EMSCRIPTEN)
@ -26,19 +28,19 @@ set(TWN_TARGET townengine CACHE INTERNAL "")
set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "") set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "")
# feature configuration, set them with -DFEATURE=ON/OFF in cli # feature configuration, set them with -DFEATURE=ON/OFF in cli
option(TWN_FEATURE_DYNLIB_GAME "Enable dynamic library loading support" ON) option(TWN_USE_AMALGAM "Enable use of twn_amalgam.c as a single compilation unit" ON)
option(TWN_FEATURE_PUSH_AUDIO "Enable frame based audio push for easy realtime audio" ON) option(TWN_FEATURE_DYNLIB_GAME "Enable dynamic library loading support" ON)
option(TWN_USE_AMALGAM "Enable use of twn_amalgam.c as a single compilation unit" ON) set(TWN_OUT_DIR ${CMAKE_SOURCE_DIR} CACHE PATH "Artifact destination")
# todo: figure out how to compile for dynamic linking instead # todo: figure out how to compile for dynamic linking instead?
if(EMSCRIPTEN) if(HAIKU OR EMSCRIPTEN)
if(TWN_FEATURE_DYNLIB_GAME) if(TWN_FEATURE_DYNLIB_GAME)
message(WARNING "TWN_FEATURE_DYNLIB_GAME is set, but not supported - it is turned off") message(WARNING "TWN_FEATURE_DYNLIB_GAME is set, but not supported - it is turned off")
set(TWN_FEATURE_DYNLIB_GAME OFF CACHE INTERNAL "") set(TWN_FEATURE_DYNLIB_GAME OFF CACHE INTERNAL "")
endif() endif()
endif() endif()
if(HAIKU) if(HAIKU OR EMSCRIPTEN)
if(TWN_SANITIZE) if(TWN_SANITIZE)
message(WARNING "TWN_SANITIZE is set, but not supported - it is turned off") message(WARNING "TWN_SANITIZE is set, but not supported - it is turned off")
set(TWN_SANITIZE OFF CACHE INTERNAL "") set(TWN_SANITIZE OFF CACHE INTERNAL "")
@ -64,11 +66,7 @@ set(PHYSFS_ARCHIVE_7Z OFF CACHE INTERNAL "")
add_subdirectory(third-party/physfs ${CMAKE_CURRENT_BINARY_DIR}/third-party/physfs SYSTEM) add_subdirectory(third-party/physfs ${CMAKE_CURRENT_BINARY_DIR}/third-party/physfs SYSTEM)
add_subdirectory(third-party/libxm ${CMAKE_CURRENT_BINARY_DIR}/third-party/libxm SYSTEM) add_subdirectory(third-party/libxm ${CMAKE_CURRENT_BINARY_DIR}/third-party/libxm SYSTEM)
if(EMSCRIPTEN) set(TWN_RENDERING_API OPENGL_15)
set(TWN_RENDERING_API WEBGL1)
else()
set(TWN_RENDERING_API OPENGL_15)
endif()
if(TWN_RENDERING_API MATCHES OPENGL_15) if(TWN_RENDERING_API MATCHES OPENGL_15)
set(SYSTEM_SOURCE_FILES ${SYSTEM_SOURCE_FILES} set(SYSTEM_SOURCE_FILES ${SYSTEM_SOURCE_FILES}
@ -84,7 +82,6 @@ set(TWN_THIRD_PARTY_SOURCE_FILES
set(TWN_NONOPT_SOURCE_FILES set(TWN_NONOPT_SOURCE_FILES
src/twn_stb.c src/twn_stb.c
src/twn_main.c
src/twn_context.c include/twn_context.h src/twn_context.c include/twn_context.h
src/twn_audio.c include/twn_audio.h src/twn_audio.c include/twn_audio.h
@ -97,6 +94,7 @@ set(TWN_NONOPT_SOURCE_FILES
src/twn_filewatch.c src/twn_filewatch_c.h src/twn_filewatch.c src/twn_filewatch_c.h
src/twn_filewatch.c src/twn_filewatch_c.h src/twn_filewatch.c src/twn_filewatch_c.h
src/twn_timer.c src/twn_timer_c.h src/twn_timer.c src/twn_timer_c.h
src/twn_workers.c src/twn_workers_c.h
src/rendering/twn_draw.c src/rendering/twn_draw_c.h src/rendering/twn_draw.c src/rendering/twn_draw_c.h
src/rendering/twn_quads.c src/rendering/twn_quads.c
@ -107,6 +105,8 @@ set(TWN_NONOPT_SOURCE_FILES
src/rendering/twn_billboards.c src/rendering/twn_billboards.c
src/rendering/twn_circles.c src/rendering/twn_circles.c
src/rendering/twn_skybox.c src/rendering/twn_skybox.c
src/rendering/twn_models.c
src/rendering/twn_lines.c
) )
set(TWN_SOURCE_FILES set(TWN_SOURCE_FILES
@ -136,12 +136,10 @@ set_target_properties(${TWN_TARGET} PROPERTIES
C_STANDARD_REQUIRED ON C_STANDARD_REQUIRED ON
C_EXTENSIONS ON) # extensions are required by stb_ds.h C_EXTENSIONS ON) # extensions are required by stb_ds.h
target_compile_definitions(${TWN_TARGET} PRIVATE $<$<BOOL:${TWN_FEATURE_PUSH_AUDIO}>:TWN_FEATURE_PUSH_AUDIO>)
# precompile commonly used not-so-small headers # precompile commonly used not-so-small headers
target_precompile_headers(${TWN_TARGET} PRIVATE target_precompile_headers(${TWN_TARGET} PRIVATE
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h> $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h>
${SDL2_INCLUDE_DIR}/SDL.h $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:${SDL2_INCLUDE_DIR}/SDL.h>
third-party/physfs/src/physfs.h) third-party/physfs/src/physfs.h)
@ -153,14 +151,14 @@ function(give_options_without_warnings target)
-fno-signed-zeros -fno-signed-zeros
-fno-trapping-math -fno-trapping-math
-freciprocal-math -freciprocal-math
# TODO: require packaging for web case
$<$<BOOL:${EMSCRIPTEN}>:-sUSE_SDL=2>) $<$<BOOL:${EMSCRIPTEN}>:-sUSE_SDL=2>)
set(BUILD_FLAGS_RELEASE set(BUILD_FLAGS_RELEASE
-O3 -O3
-flto=$<IF:$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>,thin,auto> -flto=$<IF:$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>,thin,auto>
-mavx -mavx2 $<$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},AMD64>:-sse2 -mavx -mavx2>
-fdata-sections $<$<BOOL:${EMSCRIPTEN}>:-msimd128 -mrelaxed-simd>
-ffunction-sections
-funroll-loops -funroll-loops
-fomit-frame-pointer -fomit-frame-pointer
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-s>) $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-s>)
@ -170,8 +168,20 @@ function(give_options_without_warnings target)
-g3 -g3
-gdwarf -gdwarf
-fno-omit-frame-pointer -fno-omit-frame-pointer
$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address> $<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>)
$<$<BOOL:${EMSCRIPTEN}>:-gsource-map>)
set(LINK_FLAGS
-Bsymbolic-functions
$<$<BOOL:${EMSCRIPTEN}>:-sLEGACY_GL_EMULATION -sGL_FFP_ONLY -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2
-sENVIRONMENT=web -sDEFAULT_TO_CXX=0>
$<$<BOOL:${EMSCRIPTEN}>:--preload-file ${TWN_OUT_DIR}/data@data -sALLOW_MEMORY_GROWTH>
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:-Wl,--as-needed>
$<$<BOOL:${LINUX}>:-Wl,--hash-style=gnu>)
set(LINK_FLAGS_RELEASE
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-Wl,--strip-all>
${BUILD_FLAGS_RELEASE})
if (CMAKE_C_COMPILER_LINKER_ID MATCHES GNU OR CMAKE_C_COMPILER_LINKER_ID MATCHES GNUgold) if (CMAKE_C_COMPILER_LINKER_ID MATCHES GNU OR CMAKE_C_COMPILER_LINKER_ID MATCHES GNUgold)
set(THINLTO_USAGE "-plugin-opt,") set(THINLTO_USAGE "-plugin-opt,")
@ -193,11 +203,9 @@ function(give_options_without_warnings target)
target_link_options(${target} PUBLIC target_link_options(${target} PUBLIC
${BUILD_FLAGS} ${BUILD_FLAGS}
# -Wl,--no-undefined # TODO: use later for implementing no-libc ${LINK_FLAGS}
$<$<CONFIG:Release>:${BUILD_FLAGS_RELEASE}> $<$<CONFIG:Release>:${LINK_FLAGS_RELEASE}>
$<$<CONFIG:Debug>:${BUILD_FLAGS_DEBUG}> $<$<CONFIG:Debug>:${BUILD_FLAGS_DEBUG}>)
-Bsymbolic-functions
$<$<BOOL:${LINUX}>:-Wl,--hash-style=gnu>)
get_target_property(target_type ${target} TYPE) get_target_property(target_type ${target} TYPE)
if (target_type MATCHES SHARED_LIBRARY) if (target_type MATCHES SHARED_LIBRARY)
@ -206,6 +214,9 @@ function(give_options_without_warnings target)
target_link_options(${target} PUBLIC target_link_options(${target} PUBLIC
$<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>) $<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>)
elseif(CMAKE_BUILD_TYPE MATCHES Release)
target_compile_options(${target} PUBLIC
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-mllvm=--enable-gvn-hoist>)
endif() endif()
target_compile_definitions(${target} PRIVATE target_compile_definitions(${target} PRIVATE
@ -236,6 +247,7 @@ function(give_options target)
-Werror=vla -Werror=vla
-Wno-missing-field-initializers -Wno-missing-field-initializers
-Wunused-result -Wunused-result
-Wno-pre-c11-compat
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-Wcast-align=strict>) $<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-Wcast-align=strict>)
target_compile_options(${target} PRIVATE target_compile_options(${target} PRIVATE
@ -254,6 +266,7 @@ function(include_deps target)
third-party/stb third-party/stb
third-party/dmon third-party/dmon
third-party/tomlc99 third-party/tomlc99
third-party/fast_obj
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include>) $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include>)
list(TRANSFORM THIRD_PARTY_INCLUDES PREPEND ${TWN_ROOT_DIR}/) list(TRANSFORM THIRD_PARTY_INCLUDES PREPEND ${TWN_ROOT_DIR}/)
@ -267,19 +280,40 @@ endfunction()
function(link_deps target) function(link_deps target)
target_link_libraries(${target} PUBLIC target_link_libraries(${target} PUBLIC
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:SDL2::SDL2> $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:SDL2::SDL2>
$<$<NOT:$<OR:$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>,$<BOOL:${EMSCRIPTEN}>>>:SDL2::SDL2main>
physfs-static physfs-static
xms) xms)
target_include_directories(${target} PUBLIC ${SDL2_INCLUDE_DIRS}) target_include_directories(${target} PUBLIC ${SDL2_INCLUDE_DIRS})
endfunction() endfunction()
function(use_townengine target sources output_directory) function(put_townengine output_directory)
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
add_executable(${target} ${TWN_ROOT_DIR}/src/twn_main.c)
target_link_options(${target} PRIVATE $<$<BOOL:${LINUX}>:-Wl,-rpath,$ORIGIN/>)
set_target_properties(${TWN_TARGET} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${output_directory})
set_target_properties(${target} PROPERTIES
OUTPUT_NAME ${target}
LIBRARY_OUTPUT_DIRECTORY ${output_directory}
RUNTIME_OUTPUT_DIRECTORY ${output_directory})
give_options(${target})
include_deps(${target})
link_deps(${target})
target_link_libraries(${target} PUBLIC ${TWN_TARGET})
endfunction()
function(use_townengine sources output_directory)
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
if(TWN_FEATURE_DYNLIB_GAME) if(TWN_FEATURE_DYNLIB_GAME)
# game shared library, for reloading # game shared library, for reloading
add_library(${target}_game SHARED ${sources}) add_library(${target}_game SHARED ${sources})
give_options(${target}_game) give_options(${target}_game)
include_deps(${target}_game) include_deps(${target}_game)
target_link_libraries(${target}_game PUBLIC $<$<NOT:$<BOOL:${EMSCRIPTEN}>>:SDL2::SDL2> ${TWN_TARGET}) target_link_libraries(${target}_game PUBLIC ${TWN_TARGET})
set_target_properties(${target}_game PROPERTIES set_target_properties(${target}_game PROPERTIES
OUTPUT_NAME game OUTPUT_NAME game
LIBRARY_OUTPUT_DIRECTORY ${output_directory} LIBRARY_OUTPUT_DIRECTORY ${output_directory}
@ -309,7 +343,7 @@ function(use_townengine target sources output_directory)
endif() endif()
target_compile_options(${target} PRIVATE target_compile_options(${target} PRIVATE
$<$<BOOL:${EMSCRIPTEN}>:--shell-file ${TWN_ROOT_DIR}/shell_minimal.html>) $<$<BOOL:${EMSCRIPTEN}>:--shell-file ${TWN_ROOT_DIR}/src/shell_minimal.html>)
# system libraries # system libraries
find_library(MATH_LIBRARY m) find_library(MATH_LIBRARY m)
@ -338,5 +372,18 @@ link_deps(twn_third_parties)
give_options(${TWN_TARGET}) give_options(${TWN_TARGET})
include_deps(${TWN_TARGET}) include_deps(${TWN_TARGET})
link_deps(${TWN_TARGET}) link_deps(${TWN_TARGET})
target_link_libraries(${TWN_TARGET} PUBLIC twn_third_parties)
target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src) target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src)
target_link_libraries(${TWN_TARGET} PUBLIC
twn_third_parties
${CMAKE_CURRENT_BINARY_DIR}/font.o)
# embed resources
# TODO: think of a portable way to compress/decompress them
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/font.o
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND} -E env CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} sh bin/prep-embed.sh
DEPENDS share/assets/Dernyns256.ttf)
add_custom_target(asset-compilation ALL DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/font.o)

View File

@ -6,11 +6,11 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug) set(CMAKE_BUILD_TYPE Debug)
endif() endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES set(SOURCE_FILES
game.c game.c
state.h state.h
) )
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" source = "../../../data"
name = "common-data" name = "common-data"

View File

@ -69,8 +69,8 @@ void game_tick(void)
ctx.udata = calloc(1, sizeof(State)); ctx.udata = calloc(1, sizeof(State));
} }
input_action("add_a_bit", CONTROL_LEFT_MOUSE); input_action("add_a_bit", "LCLICK");
input_action("add_a_lot", CONTROL_RIGHT_MOUSE); input_action("add_a_lot", "RCLICK");
State *state = ctx.udata; State *state = ctx.udata;

View File

@ -0,0 +1 @@
castledoors.png - https://opengameart.org/content/castle-door - CC-BY 3.0

BIN
apps/demos/crawl/data/assets/brick.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/demos/crawl/data/assets/castledoors.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/demos/crawl/data/assets/lever.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/demos/crawl/data/assets/mossy_rock.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/demos/crawl/data/assets/pebbles.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,37 @@
@map
-- Grid of symbols defined by @classes section.
#####
#.@.#
#...#
##.######
#...X...#
#./.#####
#####
@classes
-- Defines classes under symbols, which could have properties attached.
# stone_wall
wall_texture : /assets/brick.png
solid : 1
. stone_floor
tile_texture : /assets/mossy_rock.png
@ player_spawn
unique : 1
face : south
hold : torch
tile_texture : /assets/mossy_rock.png
X door
open_on_signal : sg_torch0
tile_texture : /assets/mossy_rock.png
face : horizon
face_texture : /assets/castledoors.png
/ lever
on_interact_emit : sg_torch0
tile_texture : /assets/mossy_rock.png
face : observer
face_texture : /assets/lever.png
@meta
-- Arbitrary sections could be defined with value pairs.
description : Test Level! Just two square rooms and a tunnel opened by lever.

View File

@ -0,0 +1,71 @@
require("level")
require("render")
function lerp(a, b, x)
return a + ((b - a) * x)
end
function qlerp(a, b, x)
return lerp(a, b, x * x)
end
function game_tick()
if ctx.udata == nil then
ctx.udata = {
level = load_level("levels/00.lvl")
}
ctx.udata.player = {
position = ctx.udata.level.classes.player_spawn.position,
position_lerp = ctx.udata.level.classes.player_spawn.position,
direction = { x = 1, y = 0, z = 0 },
direction_lerp = { x = 1, y = 0, z = 0 },
}
end
input_action { control = "A", name = "turn_left" }
input_action { control = "D", name = "turn_right" }
input_action { control = "W", name = "walk_forward" }
input_action { control = "S", name = "walk_backward" }
if input_action_just_released { name = "turn_left" } then
ctx.udata.player.direction = { x = ctx.udata.player.direction.z,
y = ctx.udata.player.direction.y,
z =-ctx.udata.player.direction.x }
end
if input_action_just_released { name = "turn_right" } then
ctx.udata.player.direction = { x =-ctx.udata.player.direction.z,
y = ctx.udata.player.direction.y,
z = ctx.udata.player.direction.x }
end
local move = { x = 0, y = 0 }
if input_action_just_released { name = "walk_forward" } then
move = { x = move.x + ctx.udata.player.direction.x, y = move.y + ctx.udata.player.direction.z }
end
if input_action_just_released { name = "walk_backward" } then
move = { x = move.x - ctx.udata.player.direction.x, y = move.y - ctx.udata.player.direction.z }
end
if ctx.udata.level.grid[ctx.udata.player.position.y + move.y][ctx.udata.player.position.x + move.x].solid ~= nil then
move = { x = 0, y = 0 }
end
ctx.udata.player.position = { x = ctx.udata.player.position.x + move.x, y = ctx.udata.player.position.y + move.y }
ctx.udata.player.position_lerp.x = qlerp(ctx.udata.player.position_lerp.x, ctx.udata.player.position.x, ctx.frame_duration * 30)
ctx.udata.player.position_lerp.y = qlerp(ctx.udata.player.position_lerp.y, ctx.udata.player.position.y, ctx.frame_duration * 30)
ctx.udata.player.direction_lerp.x = qlerp(ctx.udata.player.direction_lerp.x, ctx.udata.player.direction.x, ctx.frame_duration * 40)
ctx.udata.player.direction_lerp.z = qlerp(ctx.udata.player.direction_lerp.z, ctx.udata.player.direction.z, ctx.frame_duration * 40)
draw_camera {
position = {
x = ctx.udata.player.position_lerp.x + 0.5 - ctx.udata.player.direction.x / 2,
y = 0.5,
z = ctx.udata.player.position_lerp.y + 0.5 - ctx.udata.player.direction.z / 2,
},
direction = ctx.udata.player.direction_lerp,
}
render_dungeon(ctx.udata.level)
end

View File

@ -0,0 +1,91 @@
function load_level(file)
local f = file_read { file = file }
local result = {
-- templates to fill the grid with
classes = {
-- predefined empty tile
void = { },
},
-- symbol to class lookup table
glossary = {
[" "] = "void",
},
-- grid consists of expanded classes, of size dimensions
grid = {},
-- map consists of original rows of symbols that the grid is constructed from
map = {},
-- maximum extends of the map, unspecified tiles are filled with "void"
size = { x = 0, y = 0 },
}
-- iterate over lines
local section, subsection = "none", "none"
local from = 1
local start, limit = string.find(f, "\n", from)
while start do
local line = string.sub(f, from, start - 1)
-- skip over
if #line == 0 or line:find("^%-%-%s*") then
goto skip
-- start new section
elseif line:find("^@%g+") then
section = line:sub(2); subsection = "none"
-- decode map one line at a time
elseif section == "map" then
if result.size.x < #line then
result.size.x = #line
end
result.map[#result.map + 1] = line
-- templates to expand
elseif section == "classes" then
-- properties
if line:find("^ %g+") then
local _, _, property, value = line:find("^ (%g+)%s?:%s?(.*)")
result.classes[subsection][property] = value
goto skip
end
local symbol, classname = line:sub(1,1), line:sub(3)
result.classes[classname] = {
symbol = symbol,
}
result.glossary[symbol] = classname
subsection = classname
elseif section ~= "none" then
local _, _, property, value = line:find("^(%g+)%s?:%s?(.*)")
if result[section] == nil then
result[section] = {}
end
result[section][property] = value
end
::skip::
from = limit + 1
start, limit = string.find(f, "\n", from)
end
-- post process, expand map to grid
for y = 1, #result.map do
result.grid[y] = {}
for x = 1, result.size.x do
-- past defined for line
local symbol
if x > #result.map[y] then
symbol = " "
else
symbol = result.map[y]:sub(x,x)
end
local class = result.classes[result.glossary[symbol]]
if class["unique"] ~= nil then
class.position = { x = x, y = y }
end
result.grid[y][x] = class
::continue::
end
end
result.size.y = #result.map
print(result.meta.description)
return result
end

View File

@ -0,0 +1,88 @@
-- if this is too wasteful, one could check nerby tiles to see whether faces could be visible
-- more robust solution would be to travel the level from observer point of view
function render_dungeon(dungeon)
for y = 1, dungeon.size.y do
for x = 1, dungeon.size.x do
if dungeon.grid[y][x].wall_texture ~= nil then
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = x, y = 1, z = y },
v2 = { x = x, y = 0, z = y },
v1 = { x = x + 1, y = 0, z = y },
v0 = { x = x + 1, y = 1, z = y },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = x + 1, y = 1, z = y },
v2 = { x = x + 1, y = 0, z = y },
v1 = { x = x + 1, y = 0, z = y + 1 },
v0 = { x = x + 1, y = 1, z = y + 1 },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = x + 1, y = 1, z = y + 1 },
v2 = { x = x + 1, y = 0, z = y + 1 },
v1 = { x = x, y = 0, z = y + 1 },
v0 = { x = x, y = 1, z = y + 1 },
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].wall_texture,
v3 = { x = x, y = 1, z = y + 1 },
v2 = { x = x, y = 0, z = y + 1 },
v1 = { x = x, y = 0, z = y },
v0 = { x = x, y = 1, z = y },
texture_region = { w = 128, h = 128 },
}
elseif dungeon.grid[y][x].tile_texture ~= nil then
draw_quad {
texture = dungeon.grid[y][x].tile_texture,
v0 = { x = x + 1, y = 0, z = y },
v1 = { x = x, y = 0, z = y },
v2 = { x = x, y = 0, z = y + 1 },
v3 = { x = x + 1, y = 0, z = y + 1},
texture_region = { w = 128, h = 128 },
}
draw_quad {
texture = dungeon.grid[y][x].tile_texture,
v3 = { x = x + 1, y = 1, z = y },
v2 = { x = x, y = 1, z = y },
v1 = { x = x, y = 1, z = y + 1 },
v0 = { x = x + 1, y = 1, z = y + 1},
texture_region = { w = 128, h = 128 },
}
end
if dungeon.grid[y][x].face_texture ~= nil then
if dungeon.grid[y][x].face == "horizon" then
draw_quad {
texture = dungeon.grid[y][x].face_texture,
v3 = { x = x + 1, y = 1, z = y },
v2 = { x = x + 1, y = 0, z = y },
v1 = { x = x + 1, y = 0, z = y + 1 },
v0 = { x = x + 1, y = 1, z = y + 1 },
texture_region = { w = 64, h = 64 },
}
draw_quad {
texture = dungeon.grid[y][x].face_texture,
v3 = { x = x, y = 1, z = y + 1 },
v2 = { x = x, y = 0, z = y + 1 },
v1 = { x = x, y = 0, z = y },
v0 = { x = x, y = 1, z = y },
texture_region = { w = 64, h = 64 },
}
elseif dungeon.grid[y][x].face == "observer" then
draw_billboard {
texture = dungeon.grid[y][x].face_texture,
position = { x = x + 0.5, y = 0.5, z = y + 0.5 },
size = { x = 0.5, y = 0.5 },
}
end
end
end
end
end

View File

@ -14,6 +14,7 @@ dev_id = "you"
# Game runtime details # Game runtime details
[game] [game]
resolution = [ 640, 480 ] resolution = [ 640, 480 ]
interpreter = "$TWNROOT/apps/twnlua"
#debug = true #debug = true
# Engine tweaks. You probably don't need to change these # Engine tweaks. You probably don't need to change these

View File

@ -6,7 +6,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug) set(CMAKE_BUILD_TYPE Debug)
endif() endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES set(SOURCE_FILES
game.c game.c
@ -20,4 +20,4 @@ set(SOURCE_FILES
scenes/ingame.c scenes/ingame.h scenes/ingame.c scenes/ingame.h
) )
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${TWN_OUT_DIR})

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" # where does it come from, might be an url source = "../../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -22,17 +22,13 @@ void game_tick(void) {
State *state = ctx.udata; State *state = ctx.udata;
input_action("debug_toggle", CONTROL_BACKSPACE); input_action("debug_toggle", "BACKSPACE");
input_action("debug_dump_atlases", CONTROL_HOME); input_action("debug_dump_atlases", "HOME");
if (input_action_just_pressed("debug_toggle")) { if (input_action_just_pressed("debug_toggle")) {
ctx.debug = !ctx.debug; ctx.debug = !ctx.debug;
} }
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}
state->scene->tick(state); state->scene->tick(state);
/* there's a scene switch pending, we can do it now that the tick is done */ /* there's a scene switch pending, we can do it now that the tick is done */

View File

@ -11,9 +11,9 @@
static void update_timers(Player *player) { static void update_timers(Player *player) {
player->jump_air_timer = timer_tick_frames(player->jump_air_timer); player->jump_air_timer = player->jump_air_timer - 1 <= 0 ? 0 : player->jump_air_timer - 1;
player->jump_coyote_timer = timer_tick_frames(player->jump_coyote_timer); player->jump_coyote_timer = player->jump_coyote_timer - 1 <= 0 ? 0 : player->jump_coyote_timer - 1;
player->jump_buffer_timer = timer_tick_frames(player->jump_buffer_timer); player->jump_buffer_timer = player->jump_buffer_timer - 1 <= 0 ? 0 : player->jump_buffer_timer - 1;
} }

View File

@ -11,12 +11,17 @@
static void ingame_tick(State *state) { static void ingame_tick(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
input_action("player_left", CONTROL_A); input_action("player_left", "A");
input_action("player_right", CONTROL_D); input_action("player_right", "D");
input_action("player_forward", CONTROL_W); input_action("player_forward", "W");
input_action("player_backward", CONTROL_S); input_action("player_backward", "S");
input_action("player_jump", CONTROL_SPACE); input_action("player_jump", "SPACE");
input_action("player_run", CONTROL_LSHIFT); input_action("player_run", "LSHIFT");
draw_camera_2d((Vec2){ scn->player->rect.x + scn->player->rect.w / 2 - ctx.resolution.x / 2,
scn->player->rect.y + scn->player->rect.h / 2 - ctx.resolution.y / 2 },
0, 1
);
world_drawdef(scn->world); world_drawdef(scn->world);
player_calc(scn->player); player_calc(scn->player);

View File

@ -13,7 +13,7 @@ static void title_tick(State *state) {
SceneTitle *scn = (SceneTitle *)state->scene; SceneTitle *scn = (SceneTitle *)state->scene;
(void)scn; (void)scn;
input_action("ui_accept", CONTROL_RETURN); input_action("ui_accept", "ENTER");
if (input_action_just_pressed("ui_accept")) { if (input_action_just_pressed("ui_accept")) {
switch_to(state, ingame_scene); switch_to(state, ingame_scene);
@ -27,7 +27,6 @@ static void title_tick(State *state) {
size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1; size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1;
char *text_str = malloc(text_str_len); char *text_str = malloc(text_str_len);
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number); snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
free(text_str);
const char *font = "fonts/kenney-pixel.ttf"; const char *font = "fonts/kenney-pixel.ttf";
float text_h = 32; float text_h = 32;

View File

@ -6,15 +6,14 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug) set(CMAKE_BUILD_TYPE Debug)
endif() endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES set(SOURCE_FILES
game.c game.c
state.h state.h
scenes/scene.c scenes/scene.h scenes/scene.c scenes/scene.h
scenes/title.c scenes/title.h
scenes/ingame.c scenes/ingame.h scenes/ingame.c scenes/ingame.h
) )
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../../common-data" # where does it come from, might be an url source = "../../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -1,6 +1,5 @@
#include "state.h" #include "state.h"
#include "scenes/scene.h" #include "scenes/scene.h"
#include "scenes/title.h"
#include "scenes/ingame.h" #include "scenes/ingame.h"
#include "twn_game_api.h" #include "twn_game_api.h"
@ -18,23 +17,19 @@ void game_tick(void) {
State *state = ctx.udata; State *state = ctx.udata;
state->ctx = &ctx; state->ctx = &ctx;
state->scene = title_scene(state); state->scene = ingame_scene(state);
} }
} }
State *state = ctx.udata; State *state = ctx.udata;
input_action("debug_toggle", CONTROL_BACKSPACE); input_action("debug_toggle", "BACKSPACE");
input_action("debug_dump_atlases", CONTROL_HOME); input_action("debug_dump_atlases", "HOME");
if (input_action_just_pressed("debug_toggle")) { if (input_action_just_pressed("debug_toggle")) {
ctx.debug = !ctx.debug; ctx.debug = !ctx.debug;
} }
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}
state->scene->tick(state); state->scene->tick(state);
/* there's a scene switch pending, we can do it now that the tick is done */ /* there's a scene switch pending, we can do it now that the tick is done */

View File

@ -1,5 +1,4 @@
#include "ingame.h" #include "ingame.h"
#include "title.h"
#include "scene.h" #include "scene.h"
#include "twn_game_api.h" #include "twn_game_api.h"
@ -13,21 +12,298 @@
#define TERRAIN_FREQUENCY 0.15f #define TERRAIN_FREQUENCY 0.15f
#define TERRAIN_DISTANCE 64 #define TERRAIN_RADIUS 128
#define GRASS_RADIUS 16
#define TERRAIN_DISTANCE (TERRAIN_RADIUS * 2)
#define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2) #define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2)
#define PLAYER_HEIGHT 0.6f #define PLAYER_HEIGHT 0.6f
#define TREE_DENSITY 0.03f
#define G_CONST 10.0f
/* TODO: pregenerate grid of levels of detail */
static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE]; static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE];
/* vehicle sim ! */
/* https://www.youtube.com/watch?v=pwbwFdWBkU0 */
/* representation is a "jelly" box with spring configuration trying to make it coherent */
/* == springs == */
/* damped spring: F = -kx - cv */
/* x = length(p1-p0) - at_rest_length */
/* v = dot(v1 - v0, unit(p1-p0)) */
/* F = (-kx - cv) * unit(p1-p0) */
/* v += (F/m)*t */
/* one points gains positive F, other negative F, to come together */
/* == ground interaction == */
/* if point is under terrain, then apply this: */
/* x = y difference on point and ground */
/* -x(n) = x * normal */
/* -v(n) = dot(v, normal) */
/* -F = (-kx(n)-cv(n)) * normal */
/* == friction == */
/* v(o)/F(o) are perpendicular to slope (x - x(n)) */
/* if v(o) == 0, then */
/* -- at rest, static friction overcomes */
/* if F(o) <= f(s)*x(n), F(o) = 0 */
/* else, F(o) -= f(k) * x(n) */
/* else if length(v(o) + (F(o)/m)*t) <= (f(k)*x(n)*t), v(o) = 0 */
/* else, F = -unit(v(o)*f(k)*x(n)) */
#define VEHICLE_MASS 200.0f
#define VEHICLE_LENGTH 3.0f
#define VEHICLE_WIDTH 1.7f
#define VEHICLE_HEIGHT 1.3f
/* spring constant */
#define VEHICLE_SPRING_K 22000.0f
#define VEHICLE_SPRING_K_SHOCK 18000.0f
#define VEHICLE_SPRING_GK 70000.0f
/* damping constant */
#define VEHICLE_SPRING_C 800.0f
#define VEHICLE_SPRING_C_SHOCK 500.0f
#define VEHICLE_SPRING_GC 100.0f
#define VEHICLE_FRICTION_S 1000.0f
#define VEHICLE_FRICTION_K 110.0f
#define VEHICLE_FRICTION_V 4000.0f
/* TODO: shock springs, that are more loose, which are used to simulate the wheels */
/* initial, ideal corner positions */
static const Vec3 vbpi[8] = {
[0] = { 0, 0, 0 },
[1] = { VEHICLE_LENGTH, 0, 0 },
[2] = { VEHICLE_LENGTH, 0, VEHICLE_WIDTH },
[3] = { 0, 0, VEHICLE_WIDTH },
[4] = { 0, VEHICLE_HEIGHT, 0 },
[5] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, 0 },
[6] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, VEHICLE_WIDTH },
[7] = { 0, VEHICLE_HEIGHT, VEHICLE_WIDTH },
};
/* corner positions in simulation */
static Vec3 vbp[8] = { vbpi[0], vbpi[1], vbpi[2], vbpi[3],
vbpi[4], vbpi[5], vbpi[6], vbpi[7], };
/* corner velocities */
static Vec3 vbv[8];
/* springs */
static uint8_t vbs[28][2] = {
{0, 1}, {4, 5}, {0, 4},
{1, 2}, {5, 6}, {1, 5},
{2, 3}, {6, 7}, {2, 6},
{3, 0}, {7, 4}, {3, 7},
{0, 2}, {0, 5}, {0, 7},
{1, 3}, {1, 6}, {1, 4},
{4, 6}, {2, 7}, {2, 5},
{5, 7}, {3, 4}, {3, 6},
{0, 6}, {1, 7}, {2, 4}, {3, 5},
};
/* ackermann steering geometry */
static float vehicle_turning_extend;
static float vehicle_turning_speed = 0.12f;
static float vehicle_turning_extend_limit = VEHICLE_WIDTH * 1.75f;
static float height_at(SceneIngame *scn, Vec2 position);
static Vec3 normal_at(SceneIngame *scn, Vec2 position);
static inline float clampf(float f, float min, float max) {
const float t = f < min ? min : f;
return t > max ? max : t;
}
static void draw_vehicle(SceneIngame *scn) {
for (size_t i = 0; i < 12; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 255, 255, 255});
for (size_t i = 12; i < 24; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){200, 200, 200, 255});
for (size_t i = 24; i < 28; ++i)
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 125, 125, 255});
}
static void process_vehicle(SceneIngame *scn) {
/* apply gravity */
Vec3 Facc[8] = {0};
/* steering */
bool steered = false;
if (input_action_pressed("player_left")) {
vehicle_turning_extend -= vehicle_turning_speed;
steered = true;
}
if (input_action_pressed("player_right")) {
vehicle_turning_extend += vehicle_turning_speed;
steered = true;
}
if (!steered)
vehicle_turning_extend -= copysignf(vehicle_turning_speed * 0.9f, vehicle_turning_extend);
vehicle_turning_extend = clampf(vehicle_turning_extend, -vehicle_turning_extend_limit, vehicle_turning_extend_limit);
if (fabsf(vehicle_turning_extend) <= 0.11f)
vehicle_turning_extend = 0;
Vec3 const Fg = { .y = -VEHICLE_MASS * G_CONST };
for (size_t i = 0; i < 8; ++i)
Facc[i] = vec3_add(Facc[i], Fg);
/* apply springs */
for (size_t i = 0; i < 28; ++i) {
Vec3 const p0 = vbp[vbs[i][0]];
Vec3 const p1 = vbp[vbs[i][1]];
Vec3 const v0 = vbv[vbs[i][0]];
Vec3 const v1 = vbv[vbs[i][1]];
Vec3 const pd = vec3_sub(p1, p0);
Vec3 const pn = vec3_norm(pd);
/* TODO: length at rest could be precalculated */
float const lar = vec3_length(vec3_sub(vbpi[vbs[i][1]], vbpi[vbs[i][0]]));
float const x = vec3_length(pd) - lar;
float const v = vec3_dot(vec3_sub(v1, v0), pn);
float const spring_k = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_K_SHOCK : VEHICLE_SPRING_K;
float const spring_c = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_C_SHOCK : VEHICLE_SPRING_C;
Vec3 const Fs = vec3_scale(pn, -spring_k * x - spring_c * v);
Facc[vbs[i][0]] = vec3_sub(Facc[vbs[i][0]], Fs);
Facc[vbs[i][1]] = vec3_add(Facc[vbs[i][1]], Fs);
}
/* spring and friction against the ground */
for (size_t i = 0; i < 8; ++i) {
Vec3 const p = vbp[i];
Vec3 const v = vbv[i];
float const h = height_at(scn, (Vec2){ p.x, p.z });
Vec3 const fwd = vec3_norm(vec3_sub(vbp[1], vbp[0]));
if (h >= p.y) {
/* back wheel processing: acceleration */
if (i == 0 || i == 3) {
float scale = 0;
if (scn->camera_mode == 2 && input_action_pressed("player_forward"))
scale += 1;
if (scn->camera_mode == 2 && input_action_pressed("player_backward"))
scale -= 1;
Facc[i] = vec3_add(Facc[i], vec3_scale(fwd, 6500 * scale));
}
/* normal force, for displacement */
Vec3 const n = normal_at(scn, (Vec2){ p.x, p.z });
float const xn = (h - p.y) * n.y;
float const vn = vec3_dot(v, n);
Vec3 const Fn = vec3_scale(n, -VEHICLE_SPRING_GK * xn - VEHICLE_SPRING_GC * vn);
Facc[i] = vec3_sub(Facc[i], Fn);
/* friction force, perpendicular to normal force */
/* TODO: is it right? aren't vn+vol should be = |v| */
Vec3 const von = vec3_norm(vec3_cross(n, vec3_cross(v, n)));
Vec3 const vo = vec3_scale(vec3_scale(von, vec3_length(v) - vn), -1);
float const vol = vec3_length(vo);
Vec3 const Fon = vec3_norm(vec3_cross(n, vec3_cross(Facc[i], n)));
Vec3 Fo = vec3_scale(vec3_scale(Fon, vec3_length(Facc[i]) - vec3_dot(Facc[i], n)), -1);
/* portion of total force along the surface */
float const Fol = vec3_length(Fo);
float const fkxn = VEHICLE_FRICTION_K * xn;
/* at rest, might want to start moving */
if (fabsf(0.0f - vol) <= 0.0001f) {
/* cannot overcome static friction, force along the surface is zeroed */
if (Fol <= VEHICLE_FRICTION_S * xn) { Fo = vec3_scale(Fo, -1);}
/* resist the force by friction, while starting to move */
else { Fo = vec3_sub(Fo, vec3_scale(von, fkxn));}
/* not at rest, stop accelerating along the surface */
} else if (vol + (Fol / VEHICLE_MASS) * ctx.frame_duration <= fkxn * ctx.frame_duration * 2) {
/* ugh ... */
vbv[i] = vec3_add(v, vo);
/* just apply friction */
} else {
Fo = vec3_scale(von, -fkxn * 400);
}
Facc[i] = vec3_add(Facc[i], Fo);
/* rear wheel friction */
if (i == 0 || i == 3) {
Vec3 const pn = vec3_cross(fwd, n);
Vec3 const Fp = vec3_scale(pn, vec3_dot(v, pn) * -VEHICLE_FRICTION_V);
Facc[i] = vec3_add(Facc[i], Fp);
}
/* front wheel processing */
if (i == 1 || i == 2) {
/* steering influences "center of turning", which is a point */
/* laying on line defined by rear axle */
/* front arms are rotated to be perpendicular to center of turning, */
/* which then are used to dissipate forces, thus providing control */
Vec3 const rear_bar = vec3_sub(vbp[0], vbp[3]);
Vec3 const rear_center = vec3_scale(vec3_add(vbp[0], vbp[3]), 0.5);
Vec3 a, b, r;
if (i == 1) {
a = vec3_sub(vbp[3], vbp[2]);
b = vec3_sub(rear_center, vbp[2]);
r = vbp[2];
} else {
a = vec3_sub(vbp[0], vbp[1]);
b = vec3_sub(rear_center, vbp[1]);
r = vbp[1];
}
float const arm_angle = vec3_angle(a, b);
Vec3 const turn_center = vec3_add(rear_center, vec3_scale(vec3_norm(rear_bar), vehicle_turning_extend));
Vec3 const arm = vec3_sub(r, turn_center);
Vec3 const n = vec3_norm(vec3_cross(a, b));
Vec3 const wheel = vec3_norm(vec3_rotate(arm, -arm_angle, n));
Vec3 const p = vec3_norm(vec3_cross(wheel, n));
draw_line_3d(r, vec3_add(r, p), 1, (Color){0,255,255,255});
Vec3 const Fp = vec3_scale(p, vec3_dot(v, p) * -VEHICLE_FRICTION_V);
Facc[i] = vec3_add(Facc[i], Fp);
}
}
Vec3 vd = vec3_scale(vec3_scale(Facc[i], (1.0f / VEHICLE_MASS)), ctx.frame_duration);
vbv[i] = vec3_add(vbv[i], vd);
vbp[i] = vec3_add(vbp[i], vec3_scale(vbv[i], ctx.frame_duration));
}
}
static void process_vehicle_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
Vec3 const top_center = vec3_sub(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[6]), 1.0f / 2.0f));
// Vec3 const front_center = vec3_add(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[7]), 1.0f / 2.0f));
// Vec3 const facing_direction = vec3_sub(top_center, front_center);
float yawc, yaws, pitchc, pitchs;
sincosf(scn->yaw + (float)M_PI_2, &yaws, &yawc);
sincosf(scn->pitch, &pitchs, &pitchc);
Vec3 const looking_direction = vec3_norm(((Vec3){
yawc * pitchc,
pitchs,
yaws * pitchc,
}));
Vec3 const orbit = vec3_sub(top_center, vec3_scale(looking_direction, 7.5));
draw_camera(orbit, looking_direction, (Vec3){0,1,0}, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = looking_direction;
scn->pos = top_center;
}
static void process_fly_mode(State *state) { static void process_fly_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
DrawCameraFromPrincipalAxesResult dir_and_up = DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1); draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = dir_and_up.direction;
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up)); const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.04f; /* TODO: put this in a better place */ const float speed = 0.1f; /* TODO: put this in a better place */
if (input_action_pressed("player_left")) if (input_action_pressed("player_left"))
scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed)); scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed));
@ -47,33 +323,39 @@ static void process_fly_mode(State *state) {
scn->pos.y -= speed; scn->pos.y -= speed;
} }
/* TODO: could be baked in map format */
static Vec3 normal_at(SceneIngame *scn, Vec2 position) {
int const x = (int)(floorf(position.x - scn->world_center.x));
int const y = (int)(floorf(position.y - scn->world_center.y));
float const height0 = heightmap[x][y];
float const height1 = heightmap[x + 1][y];
float const height2 = heightmap[x][y + 1];
Vec3 const a = { .x = 1, .y = height0 - height1, .z = 0 };
Vec3 const b = { .x = 0, .y = height0 - height2, .z = -1 };
return vec3_norm(vec3_cross(a, b));
}
/* TODO: don't operate on triangles, instead interpolate on quads */
static float height_at(SceneIngame *scn, Vec2 position) { static float height_at(SceneIngame *scn, Vec2 position) {
float height0, height1, height2, weight0, weight1, weight2; int const x = (int)(floorf(position.x - scn->world_center.x));
int const y = (int)(floorf(position.y - scn->world_center.y));
int const x = (int)(HALF_TERRAIN_DISTANCE + (position.x - scn->pos.x)); float const height0 = heightmap[x][y];
int const y = (int)(HALF_TERRAIN_DISTANCE + (position.y - scn->pos.z)); float const height1 = heightmap[x + 1][y];
float const height2 = heightmap[x][y + 1];
height0 = heightmap[x][y]; float const height3 = heightmap[x + 1][y + 1];
height1 = heightmap[x + 1][y + 1];
Vec2 incell = { position.x - floorf(position.x), position.y - floorf(position.y) }; Vec2 incell = { position.x - floorf(position.x), position.y - floorf(position.y) };
/* who needs barycentric coordinates, am i right? */ float const weight0 = (1 - incell.x) * (1 - incell.y);
weight0 = 1 / sqrtf(powf(incell.x, 2) + powf(incell.y, 2)); float const weight1 = ( incell.x) * (1 - incell.y);
weight1 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(1 - incell.y, 2)); float const weight2 = (1 - incell.x) * ( incell.y);
float const weight3 = ( incell.x) * ( incell.y);
/* find which triangle we're directly under */ return (height0 * weight0 + height1 * weight1 + height2 * weight2 + height3 * weight3) / (weight0 + weight1 + weight2 + weight3);
/* for this manhattan distance is sufficient */
if (incell.x + (1 - incell.y) < (1 - incell.x) + incell.y) {
height2 = heightmap[x][y + 1];
weight2 = 1 / sqrtf(powf(incell.x, 2) + powf(1 - incell.y, 2));
} else {
height2 = heightmap[x + 1][y];
weight2 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(incell.y, 2));
}
return (height0 * weight0 + height1 * weight1 + height2 * weight2) / (weight0 + weight1 + weight2);
} }
@ -81,13 +363,15 @@ static void process_ground_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
DrawCameraFromPrincipalAxesResult dir_and_up = DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1); draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
scn->looking_direction = dir_and_up.direction;
dir_and_up.direction.y = 0; dir_and_up.direction.y = 0;
dir_and_up.direction = vec3_norm(dir_and_up.direction); dir_and_up.direction = vec3_norm(dir_and_up.direction);
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up)); const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.18f; /* TODO: put this in a better place */ const float speed = 0.20f; /* TODO: put this in a better place */
Vec3 target = scn->pos; Vec3 target = scn->pos;
@ -96,7 +380,7 @@ static void process_ground_mode(State *state) {
float const height = height_at(scn, (Vec2){scn->pos.x, scn->pos.z}); float const height = height_at(scn, (Vec2){scn->pos.x, scn->pos.z});
if (target.y > height + PLAYER_HEIGHT) if (target.y > height + PLAYER_HEIGHT)
target.y = target.y - 0.4f; target.y = target.y - 0.6f;
if (target.y < height + PLAYER_HEIGHT) if (target.y < height + PLAYER_HEIGHT)
target.y = height + PLAYER_HEIGHT; target.y = height + PLAYER_HEIGHT;
@ -135,50 +419,100 @@ static void generate_terrain(SceneIngame *scn) {
float y = floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly); float y = floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly);
float height = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 1; float height = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 1;
height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 10 - 1; height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 20 - 1;
heightmap[lx][ly] = height; heightmap[lx][ly] = height;
} }
} }
scn->world_center = (Vec2){ floorf(scn->pos.x - HALF_TERRAIN_DISTANCE), floorf(scn->pos.z - HALF_TERRAIN_DISTANCE) };
}
static int32_t ceil_sqrt(int32_t const n) {
int32_t res = 1;
while(res * res < n)
res++;
return res;
}
static uint32_t adler32(const void *buf, size_t buflength) {
const uint8_t *buffer = (const uint8_t*)buf;
uint32_t s1 = 1;
uint32_t s2 = 0;
for (size_t n = 0; n < buflength; n++) {
s1 = (s1 + buffer[n]) % 65521;
s2 = (s2 + s1) % 65521;
}
return (s2 << 16) | s1;
} }
static void draw_terrain(SceneIngame *scn) { static void draw_terrain(SceneIngame *scn) {
for (int ly = TERRAIN_DISTANCE - 1; ly > 0; ly--) { /* used to cull invisible tiles over field of view (to horizon) */
for (int lx = 0; lx < TERRAIN_DISTANCE - 1; lx++) { Vec2 const d = vec2_norm((Vec2){ .x = scn->looking_direction.x, .y = scn->looking_direction.z });
int32_t x = (int32_t)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx)); float const c = cosf((float)M_PI_2 * 0.8f * 0.8f);
int32_t y = (int32_t)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
/* draw terrain in circle */
int32_t const rsi = (int32_t)TERRAIN_RADIUS * (int32_t)TERRAIN_RADIUS;
for (int32_t iy = -(int32_t)TERRAIN_RADIUS; iy <= (int32_t)TERRAIN_RADIUS - 1; ++iy) {
int32_t const dx = ceil_sqrt(rsi - (iy + (iy <= 0)) * (iy + (iy <= 0)));
for (int32_t ix = -dx; ix < dx - 1; ++ix) {
int32_t lx = ix + TERRAIN_RADIUS;
int32_t ly = iy + TERRAIN_RADIUS;
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
/* cull tiles outside of vision */
if (vec2_dot(vec2_norm((Vec2){x - scn->pos.x + d.x * 2, y - scn->pos.z + d.y * 2}), d) < c)
continue;
float d0 = heightmap[lx][ly]; float d0 = heightmap[lx][ly];
float d1 = heightmap[lx + 1][ly]; float d1 = heightmap[lx + 1][ly];
float d2 = heightmap[lx + 1][ly - 1]; float d2 = heightmap[lx + 1][ly - 1];
float d3 = heightmap[lx][ly - 1]; float d3 = heightmap[lx][ly - 1];
draw_triangle("/assets/grass.png", draw_quad("/assets/grass2.png",
(Vec3){ (float)x, d0, (float)y }, (Vec3){ (float)x, d0, (float)y },
(Vec3){ (float)x + 1, d1, (float)y },
(Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 128 },
(Vec2){ 128, 0 },
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255});
draw_triangle("/assets/grass.png",
(Vec3){ (float)x + 1, d1, (float)y }, (Vec3){ (float)x + 1, d1, (float)y },
(Vec3){ (float)x + 1, d2, (float)y - 1 }, (Vec3){ (float)x + 1, d2, (float)y - 1 },
(Vec3){ (float)x, d3, (float)y - 1 }, (Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 0 }, (Rect){ .w = 128, .h = 128 },
(Vec2){ 0, 0 },
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255}); (Color){255, 255, 255, 255});
draw_billboard("/assets/grasses/10.png", if (((float)(adler32(&((Vec2){x, y}), sizeof (Vec2)) % 100) / 100) <= TREE_DENSITY)
(Vec3){ (float)x, d0 + 0.15f, (float)y }, draw_billboard("/assets/trreez.png",
(Vec2){0.3f, 0.3f}, (Vec3){ (float)x, d0 + 1.95f, (float)y },
(Vec2){2.f, 2.f},
(Rect){0},
(Color){255, 255, 255, 255}, true);
}
}
int32_t const rsi_g = (int32_t)GRASS_RADIUS * (int32_t)GRASS_RADIUS;
for (int32_t iy = -(int32_t)GRASS_RADIUS; iy <= (int32_t)GRASS_RADIUS - 1; ++iy) {
int32_t const dx = ceil_sqrt(rsi_g - (iy + (iy <= 0)) * (iy + (iy <= 0)));
for (int32_t ix = -dx; ix < dx; ++ix) {
int32_t lx = ix + TERRAIN_RADIUS;
int32_t ly = iy + TERRAIN_RADIUS;
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
float d = heightmap[lx][ly];
draw_billboard("/assets/grasses/25.png",
(Vec3){
(float)x + (float)((adler32(&((Vec2){x, y}), sizeof (Vec2))) % 32) / 64.0f,
d + 0.2f,
(float)y + (float)((adler32(&((Vec2){y, x}), sizeof (Vec2))) % 32) / 64.0f
},
(Vec2){0.4f, 0.4f},
(Rect){0},
(Color){255, 255, 255, 255}, true); (Color){255, 255, 255, 255}, true);
} }
} }
@ -188,29 +522,31 @@ static void draw_terrain(SceneIngame *scn) {
static void ingame_tick(State *state) { static void ingame_tick(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene; SceneIngame *scn = (SceneIngame *)state->scene;
input_action("player_left", CONTROL_A); input_action("player_left", "A");
input_action("player_right", CONTROL_D); input_action("player_right", "D");
input_action("player_forward", CONTROL_W); input_action("player_forward", "W");
input_action("player_backward", CONTROL_S); input_action("player_backward", "S");
input_action("player_jump", CONTROL_SPACE); input_action("player_jump", "SPACE");
input_action("player_run", CONTROL_LSHIFT); input_action("player_run", "LSHIFT");
input_action("mouse_capture_toggle", CONTROL_ESCAPE); input_action("mouse_capture_toggle", "ESCAPE");
input_action("toggle_camera_mode", CONTROL_C); input_action("toggle_camera_mode", "C");
if (scn->mouse_captured) { if (scn->mouse_captured) {
const float sensitivity = 0.4f * (float)DEG2RAD; /* TODO: put this in a better place */ const float sensitivity = 0.4f * (float)(M_PI / 180); /* TODO: put this in a better place */
scn->yaw += (float)ctx.mouse_movement.x * sensitivity; scn->yaw += (float)ctx.mouse_movement.x * sensitivity;
scn->pitch -= (float)ctx.mouse_movement.y * sensitivity; scn->pitch -= (float)ctx.mouse_movement.y * sensitivity;
scn->pitch = clampf(scn->pitch, (float)-M_PI * 0.49f, (float)M_PI * 0.49f); scn->pitch = clampf(scn->pitch, (float)-M_PI * 0.49f, (float)M_PI * 0.49f);
} }
if (input_action_just_pressed("toggle_camera_mode")) if (input_action_just_pressed("toggle_camera_mode"))
scn->flying_camera = !scn->flying_camera; scn->camera_mode = scn->camera_mode == 2 ? 0 : scn->camera_mode + 1;
if (scn->flying_camera) { if (scn->camera_mode == 1) {
process_fly_mode(state); process_fly_mode(state);
} else { } else if (scn->camera_mode == 0) {
process_ground_mode(state); process_ground_mode(state);
} else if (scn->camera_mode) {
process_vehicle_mode(state);
} }
/* toggle mouse capture with end key */ /* toggle mouse capture with end key */
@ -220,12 +556,15 @@ static void ingame_tick(State *state) {
ctx.mouse_capture = scn->mouse_captured; ctx.mouse_capture = scn->mouse_captured;
generate_terrain(scn); generate_terrain(scn);
process_vehicle(scn);
draw_terrain(scn); draw_terrain(scn);
draw_vehicle(scn);
draw_skybox("/assets/miramar/miramar_*.tga"); draw_skybox("/assets/miramar/miramar_*.tga");
ctx.fog_color = (Color){ 140, 147, 160, 255 }; ctx.fog_color = (Color){ 140, 147, 160, 255 };
ctx.fog_density = 0.03f; ctx.fog_density = 0.015f;
} }
@ -243,7 +582,7 @@ Scene *ingame_scene(State *state) {
new_scene->mouse_captured = true; new_scene->mouse_captured = true;
m_audio(m_set(path, "music/mod65.xm"), m_audio(m_set(path, "music/woah.ogg"),
m_opt(channel, "soundtrack"), m_opt(channel, "soundtrack"),
m_opt(repeat, true)); m_opt(repeat, true));

View File

@ -12,13 +12,16 @@
typedef struct SceneIngame { typedef struct SceneIngame {
Scene base; Scene base;
Vec3 looking_direction;
Vec2 world_center;
Vec3 pos; Vec3 pos;
float yaw; float yaw;
float pitch; float pitch;
float roll; float roll;
bool mouse_captured; bool mouse_captured;
bool flying_camera; int camera_mode;
} SceneIngame; } SceneIngame;

View File

@ -1,61 +0,0 @@
#include "title.h"
#include "ingame.h"
#include "twn_game_api.h"
#include <stdio.h>
#include <stdlib.h>
static void title_tick(State *state) {
SceneTitle *scn = (SceneTitle *)state->scene;
(void)scn;
input_action("ui_accept", CONTROL_RETURN);
if (input_action_just_pressed("ui_accept")) {
switch_to(state, ingame_scene);
return;
}
m_sprite("/assets/title.png", ((Rect) {
((float)ctx.resolution.x / 2) - ((float)320 / 2), 64, 320, 128 }));
/* draw the tick count as an example of dynamic text */
size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1;
char *text_str = malloc(text_str_len);
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
const char *font = "/fonts/kenney-pixel.ttf";
float text_h = 32;
float text_w = draw_text_width(text_str, text_h, font);
draw_rectangle(
(Rect) {
.x = 0,
.y = 0,
.w = (float)text_w,
.h = (float)text_h,
},
(Color) { 0, 0, 0, 255 }
);
draw_text(text_str, (Vec2){ 0, 0 }, text_h, (Color) { 255, 255, 255, 255 }, font);
free(text_str);
}
static void title_end(State *state) {
free(state->scene);
}
Scene *title_scene(State *state) {
(void)state;
SceneTitle *new_scene = calloc(1, sizeof *new_scene);
new_scene->base.tick = title_tick;
new_scene->base.end = title_end;
return (Scene *)new_scene;
}

View File

@ -1,16 +0,0 @@
#ifndef TITLE_H
#define TITLE_H
#include "../state.h"
#include "scene.h"
typedef struct SceneTitle {
Scene base;
} SceneTitle;
Scene *title_scene(State *state);
#endif

View File

@ -6,11 +6,11 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug) set(CMAKE_BUILD_TYPE Debug)
endif() endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES set(SOURCE_FILES
game.c game.c
state.h state.h
) )
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -103,7 +103,7 @@ static void benchmark(struct state *state) {
void game_tick(void) { void game_tick(void) {
if (ctx.initialization_needed) { if (ctx.initialization_needed) {
if (!ctx.udata) { if (!ctx.udata) {
ctx.udata = ccalloc(1, sizeof (struct state)); ctx.udata = calloc(1, sizeof (struct state));
struct state *state = ctx.udata; struct state *state = ctx.udata;
state->r = 24; state->r = 24;
} }
@ -113,14 +113,16 @@ void game_tick(void) {
Vec2 const mouse_snap = {floorf(ctx.mouse_position.x / 8) * 8, floorf(ctx.mouse_position.y / 8) * 8}; Vec2 const mouse_snap = {floorf(ctx.mouse_position.x / 8) * 8, floorf(ctx.mouse_position.y / 8) * 8};
input_action("up", CONTROL_LEFT_MOUSE); input_action("up", "LCLICK");
input_action("down", CONTROL_RIGHT_MOUSE); input_action("down", "RCLICK");
if (input_action_just_pressed("up")) if (input_action_pressed("up"))
state->r += 1; state->r += 1;
if (input_action_just_pressed("down")) if (input_action_pressed("down") && state->r > 2)
state->r -= 1; state->r -= 1;
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125});
int32_t const rsi = (int32_t)state->r * (int32_t)state->r; int32_t const rsi = (int32_t)state->r * (int32_t)state->r;
int32_t acc = 1; int32_t acc = 1;
for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) { for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) {
@ -133,9 +135,8 @@ void game_tick(void) {
} }
} }
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125}); /* uncomment to see performance difference between variants */
// benchmark(state);
benchmark(state);
} }

View File

@ -1,16 +0,0 @@
cmake_minimum_required(VERSION 3.21)
project(template LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c
state.h
)
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.21)
project(twngame LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES
game.c
state.h
)
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,27 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Template"
developer = "You"
app_id = "template"
dev_id = "you"
# Game runtime details
[game]
resolution = [ 640, 480 ]
background_color = [ 255, 125, 0, 255 ]
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

View File

@ -1,7 +1,7 @@
#include "twn_game_api.h" #include "twn_game_api.h"
#include "state.h" #include "state.h"
#include <malloc.h> #include <stdlib.h>
void game_tick(void) { void game_tick(void) {
@ -10,7 +10,7 @@ void game_tick(void) {
if (ctx.initialization_needed) { if (ctx.initialization_needed) {
/* application data could be stored in ctx.udata and retrieved anywhere */ /* application data could be stored in ctx.udata and retrieved anywhere */
if (!ctx.udata) if (!ctx.udata)
ctx.udata = ccalloc(1, sizeof (struct state)); ctx.udata = calloc(1, sizeof (struct state));
} }
/* a lot of data is accessible from `ctx`, look into `townengine/context.h` for more */ /* a lot of data is accessible from `ctx`, look into `townengine/context.h` for more */

17
apps/templates/lua/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# ignore executables
*
!*.*
!*/
**/*.so
**/*.dll
**/*.exe
**/*.trace
**/*.js
**/*.wasm
**/*.wasm.map
**/*.data
**/*.html
data/scripts/twnapi.lua
build/

View File

@ -0,0 +1,7 @@
-- called every frame, with constant delta time
function game_tick()
-- ctx.udata persists on code reload
if ctx.udata == nil then
ctx.udata = {}
end
end

View File

@ -0,0 +1,27 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Template"
developer = "You"
app_id = "template"
dev_id = "you"
# Game runtime details
[game]
resolution = [ 640, 480 ]
interpreter = "$TWNROOT/apps/twnlua"
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

19
apps/templates/zig/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# ignore executables
*
!*.*
!*/
**/*.so
**/*.dll
**/*.exe
**/*.trace
**/*.js
**/*.wasm
**/*.wasm.map
**/*.data
**/*.html
data/scripts/twnapi.lua
build/
.zig-cache/
zig-out/

View File

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.21)
project(twngame LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
put_townengine(${CMAKE_CURRENT_SOURCE_DIR})
file(GLOB_RECURSE zig-sources ${CMAKE_CURRENT_SOURCE_DIR}/src/*.zig)
# TODO: support static build
# TODO: propagate release switches
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
COMMAND env zig build
DEPENDS ${TWN_TARGET} ${zig-sources}
)
add_custom_target(
zig-step ALL
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
)

View File

@ -0,0 +1,58 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// This creates a "module", which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Every executable or library we compile will be based on one or more modules.
const lib_mod = b.createModule(.{
// `root_source_file` is the Zig "entry point" of the module. If a module
// only contains e.g. external object files, you can make this `null`.
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib_mod.addIncludePath(b.path("../../../"));
lib_mod.addIncludePath(b.path("../../../include/"));
lib_mod.addLibraryPath(b.path("./"));
// Now, we will create a static library based on the module we created above.
// This creates a `std.Build.Step.Compile`, which is the build step responsible
// for actually invoking the compiler.
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = "game",
.root_module = lib_mod,
});
lib.linkSystemLibrary("townengine");
// This declares intent for the library to be installed into the standard
// location when the user invokes the "install" step (the default step when
// running `zig build`).
const install_artifact = b.addInstallArtifact(lib, .{
.dest_dir = .{
.override = .{
.custom = "../",
},
},
});
b.getInstallStep().dependOn(&install_artifact.step);
}

View File

@ -0,0 +1,27 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Zig Awesomeness"
developer = "notwanp"
app_id = "yourzigthing"
dev_id = "definatelynotwanp"
# Game runtime details
[game]
resolution = [ 640, 480 ]
background_color = [ 255, 125, 0, 255 ]
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

View File

@ -0,0 +1,24 @@
const std = @import("std");
const c = @cImport({
@cInclude("twn_game_api.h");
});
export fn game_tick() void {
tick() catch |err| {
std.log.err(@errorName(err));
};
}
export fn game_end() void {
end() catch |err| {
std.log.err(@errorName(err));
};
}
fn tick() !void {
if (c.ctx.initialization_needed) {
std.debug.print("lmao\n", .{});
}
}
fn end() !void {}

View File

@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.21)
project(twndel LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(SOURCE_FILES
tool.c
state.h
)
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

BIN
apps/tools/twndel/data/bong.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/camera.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/center.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/drop.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/grab.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/point.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/selectin.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/selectout.ogg (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
apps/tools/twndel/data/switch.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
[about]
title = "Townengine Modeling Tool"
developer = "twnteam"
app_id = "twndel"
dev_id = "twnteam"
# Game runtime details
[game]
resolution = [ 640, 480 ]
background_color = [ 255, 125, 0, 255 ]
[engine]

BIN
apps/tools/twndel/data/x.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/y.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/tools/twndel/data/z.png (Stored with Git LFS) Normal file

Binary file not shown.

118
apps/tools/twndel/state.h Normal file
View File

@ -0,0 +1,118 @@
#ifndef STATE_H
#define STATE_H
#include "twn_game_api.h"
#define POINTS_PER_METER 128
#define UNDO_STACK_SIZE 32
#define CAMERA_FOV ((float)M_PI_2 * 0.75f)
#define POINT_LIMIT 65534
#define FACE_LIMIT 2048
#define OBJECT_LIMIT 16
#define TEXTURE_LIMIT 32
#define INVALID_POINT (POINT_LIMIT+1)
#define INVALID_FACE (FACE_LIMIT+1)
#define INVALID_OBJECT (OBJECT_LIMIT+1)
#define INVALID_TEXTURE (TEXTURE_LIMIT+1)
#define CAMERA_ROTATION_SPEED 0.04f
#define CAMERA_TRANSLATION_SPEED 0.04f
#define SELECTION_SPHERE_RADIUS 32
/* should be an odd number */
#define SNAP_LINES_SHOW 7
#define SNAP_LINES_WIDTH 1.0f
#define SNAP_LINES_COLOR ((Color){200,200,200,150})
typedef struct Operation {
enum {
OPERATION_MOVE_POINT,
OPERATION_SET_TEXTURE,
OPERATION_TRIANGULATE,
} kind;
union {
struct {
uint16_t point;
int16_t delta_x;
int16_t delta_y;
int16_t delta_z;
uint8_t object;
} move_point;
struct {
uint16_t face;
int16_t delta_texture;
uint8_t object;
} set_texture;
struct {
uint16_t old_face;
uint16_t new_face;
uint8_t object;
} triangulate;
} data;
bool chained;
} Operation;
typedef struct Point {
int16_t x, y, z;
} Point;
/* TODO: store topology in terms on edge connections? might be bad, as it's stateful */
/* triangles have p3 = INVALID_POINT */
/* lines have p2, p3 = INVALID_POINT */
/* billboards have p1, p2, p3 = INVALID_POINT */
typedef struct Face {
uint16_t p[4];
/* texture origin, as point on face plane, in absolute coordinates */
int16_t tex_x, tex_y;
uint8_t texture;
uint8_t tex_scale;
} Face;
typedef struct Object {
char *name;
bool is_invisible;
Point position;
char *textures[TEXTURE_LIMIT + 1];
uint8_t textures_sz;
Point rotation;
Face faces[FACE_LIMIT];
uint16_t faces_sz;
} Object;
typedef struct State {
Operation op_stack[UNDO_STACK_SIZE];
uint32_t op_stack_ptr;
bool op_active;
Vec3 active_center;
Vec3 camera_position;
Vec3 camera_direction;
float camera_zoom;
bool camera_is_orthographic;
/* defaults to wireframe */
bool solid_display_mode;
/* positions skipped */
uint8_t grid_snap_granularity;
Point points[POINT_LIMIT];
uint16_t points_sz;
Object objects[OBJECT_LIMIT];
uint8_t objects_sz;
/* which axes are blocked and which are not */
/* order: x, y, z */
bool axis_mask[3];
char *current_texture;
uint8_t current_hovered_obj;
uint16_t current_hovered_face;
} State;
#endif

972
apps/tools/twndel/tool.c Normal file
View File

@ -0,0 +1,972 @@
#include "twn_game_api.h"
#include "state.h"
#include "twn_vec.h"
#include <SDL.h>
/* planned features: */
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
/* triangles and quads only */
/* support for billboards and flat two sided quads */
/* texture painting */
/* bones with mesh animations, snapping to grid, with no weights */
/* billboard render to specified angles */
/* 1 point light primitive lighting */
/* live edited textures are capped at 128x128 */
/* assumptions: */
/* up is always (0,1,0) */
/* preallocations everywhere */
static State state;
static bool init;
static uint8_t new_object(const char *name) {
if (state.objects_sz >= OBJECT_LIMIT)
return INVALID_OBJECT;
state.objects_sz++;
state.objects[state.objects_sz-1].name = SDL_strdup(name);
return state.objects_sz-1;
}
static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
if (state.points_sz >= POINT_LIMIT)
return INVALID_POINT;
state.points_sz++;
state.points[state.points_sz-1] = (Point){x, y, z};
return state.points_sz-1;
}
static uint16_t push_face(uint8_t object,
uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3,
uint8_t texture, uint8_t tex_scale, int16_t tex_x, int16_t tex_y)
{
Object *o = &state.objects[object];
o->faces_sz++;
o->faces[o->faces_sz-1] = (Face) {
.p = {p0, p1, p2, p3},
.texture = texture,
.tex_scale = tex_scale,
.tex_x = tex_x,
.tex_y = tex_y,
};
return o->faces_sz-1;
}
static uint8_t push_texture(uint8_t object, char *texture) {
Object *o = &state.objects[object];
/* check whether it's already here */
for (uint8_t i = 0; i < o->textures_sz; ++i)
if (SDL_strcmp(o->textures[i], texture) == 0)
return i;
o->textures_sz++;
o->textures[o->textures_sz-1] = SDL_strdup(texture);
return o->textures_sz-1;
}
/* TODO: use tombstones instead? it would be easier to maintain, by a lot */
/* note: make sure nothing depends on none */
static void pop_face(uint8_t object, uint16_t face) {
Object *o = &state.objects[object];
if (face != o->faces_sz-1)
o->faces[face] = o->faces[o->faces_sz-1];
o->faces_sz--;
}
static void push_operation(Operation operation, bool active) {
state.op_stack_ptr++;
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
state.op_stack[op] = operation;
state.op_active = active;
}
static void extend_operation(Operation operation) {
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
Operation ext = state.op_stack[op];
ext.chained = true;
state.op_stack[op] = operation;
push_operation(ext, state.op_active);
}
static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
Object *o = &state.objects[object];
return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
(o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
(o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
}
/* TODO: something is wrong, figure out how to introduce rotation to this. */
static inline Vec2 project_texture_coordinate(Vec2 origin, Vec3 plane, Vec3 point, float scale) {
Vec3 right = vec3_norm(vec3_cross(plane, fabsf(0.0f - plane.y) < 1e-9f ? (Vec3){0,1,0} : (Vec3){1,0,0}));
Vec3 up = vec3_norm(vec3_cross(right, plane));
Vec2 pp = { vec3_dot(point, right), vec3_dot(point, up) };
return vec2_scale(vec2_sub(origin, pp), scale);
}
static void render_object(uint8_t object) {
Object *o = &state.objects[object];
if (o->is_invisible)
return;
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[2] != INVALID_POINT) {
Vec3 p0 = point_to_vec3(object, f->p[0]);
Vec3 p1 = point_to_vec3(object, f->p[1]);
Vec3 p2 = point_to_vec3(object, f->p[2]);
if (state.solid_display_mode) {
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
Vec2 to = { f->tex_x / (float)POINTS_PER_METER, f->tex_y / (float)POINTS_PER_METER, };
Vec2 tul = project_texture_coordinate(to, n, p0, (float)f->tex_scale);
Vec2 tdl = project_texture_coordinate(to, n, p1, (float)f->tex_scale);
Vec2 tdr = project_texture_coordinate(to, n, p2, (float)f->tex_scale);
draw_triangle(o->textures[f->texture],
p0, p1, p2,
tul, tdl, tdr,
(Color){255,255,255,255},
(Color){255,255,255,255},
(Color){255,255,255,255});
if (f->p[3] != INVALID_POINT) {
Vec3 p3 = point_to_vec3(object, f->p[3]);
Vec2 tur = project_texture_coordinate(to, n, p3, (float)f->tex_scale);
draw_triangle(o->textures[f->texture],
p2, p3, p0,
tdr, tur, tul,
(Color){255,255,255,255},
(Color){255,255,255,255},
(Color){255,255,255,255});
}
} else {
draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
if (f->p[3] == INVALID_POINT)
draw_line_3d(p2, p0, 1, (Color){255,255,255,255});
else {
Vec3 p3 = point_to_vec3(object, f->p[3]);
draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
}
}
} else
SDL_assert_always(false);
}
}
static uint8_t new_cube(Point pos, Point size) {
uint8_t object = new_object("cube");
uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint8_t tex = push_texture(object, "/data/placeholder.png");
push_face(object, p2, p3, p0, p1, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p5, p4, p7, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p1, p0, p4, p5, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p6, p7, p3, p2, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p2, p1, p5, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
push_face(object, p0, p3, p7, p4, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
return object;
}
static void process_camera_rotation(void) {
float horizontal_rotation = 0;
float vertical_rotation = 0;
if (input_action_pressed("camera_rotate_left"))
horizontal_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_right"))
horizontal_rotation += CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_up"))
vertical_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_down"))
vertical_rotation += CAMERA_ROTATION_SPEED;
Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});
Vec3 local_position = vec3_sub(state.active_center, state.camera_position);
Vec3 new_local_position = vec3_rotate(local_position, horizontal_rotation, (Vec3){0,1,0});
state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});
Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);
/* only apply if it's in limits */
float d = vec3_dot(new_rot, (Vec3){0,-1,0});
if (fabsf(d) <= 0.999f) {
new_local_position = vec3_rotate(new_local_position, vertical_rotation, front);
state.camera_direction = new_rot;
}
state.camera_position = vec3_sub(state.active_center, new_local_position);
}
static void process_camera_translation(void) {
Vec3 right = vec3_norm(vec3_cross(state.camera_direction, (Vec3){0,1,0}));
Vec3 up = vec3_norm(vec3_cross(state.camera_direction, right));
Vec3 was = state.camera_position;
if (input_action_pressed("camera_rotate_left"))
state.camera_position = vec3_sub(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_right"))
state.camera_position = vec3_add(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_up"))
state.camera_position = vec3_sub(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
if (input_action_pressed("camera_rotate_down"))
state.camera_position = vec3_add(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
state.active_center = vec3_add(state.active_center, vec3_sub(state.camera_position, was));
draw_billboard("/data/camera.png",
vec3_add(state.camera_position, vec3_scale(state.camera_direction, vec3_length(state.camera_position))),
(Vec2){0.2f,0.2f},
(Rect){0},
(Color){255,255,255,255},
false);
/* show relation to origin */
draw_billboard("/data/center.png",
(Vec3){0},
(Vec2){0.1f,0.1f},
(Rect){0},
(Color){255,255,255,255},
false);
draw_line_3d((Vec3){0}, state.active_center, 1, (Color){255,255,255,255});
}
static void process_camera_movement(void) {
input_action("camera_rotate_left", "A");
input_action("camera_rotate_right", "D");
input_action("camera_rotate_up", "W");
input_action("camera_rotate_down", "S");
input_action("camera_lock_rotation", "SPACE");
if (input_action_pressed("camera_lock_rotation")) {
process_camera_translation();
} else {
process_camera_rotation();
}
}
static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
return draw_camera_unproject(
point,
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100 );
}
static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
/* step over every selectable object and find points closest to the view ray */
/* by constructing triangles and finding their height, from perpendicular */
uint16_t closest_point = INVALID_POINT;
uint8_t closest_obj = INVALID_OBJECT;
float closest_distance = INFINITY;
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
if (o->is_invisible)
continue;
/* TODO: is it possible to skip repeated points? does it matter? */
/* as we limit the point could we could actually have bool array preallocated for this */
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
for (uint16_t pi = 0; pi < 4; ++pi) {
if (f->p[pi] == INVALID_POINT) break;
Vec3 p = point_to_vec3(obj, f->p[pi]);
Vec3 d = vec3_sub(pos_and_ray.position, p);
Vec3 b = vec3_cross(d, pos_and_ray.direction);
float ray_dist = vec3_length(b);
if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
continue;
float dist = vec3_length(vec3_sub(pos_and_ray.position, vec3_add(p, b)));
if (dist < closest_distance) {
closest_distance = dist;
closest_obj = obj;
closest_point = f->p[pi];
}
}
}
}
if (closest_point == INVALID_POINT)
return false;
if (object_result)
*object_result = closest_obj;
if (point_result)
*point_result = closest_point;
return true;
}
/* o = vector origin */
/* v = vector direction, normalized */
/* p = any point on plane */
/* n = normal of a plane */
static bool vector_plane_intersection(Vec3 o, Vec3 v, Vec3 p, Vec3 n, Vec3 *out) {
float dot = vec3_dot(n, v);
if (fabsf(dot) > FLT_EPSILON) {
Vec3 w = vec3_sub(o, p);
float fac = -vec3_dot(n, w) / dot;
*out = vec3_add(o, vec3_scale(v, fac));
return true;
}
/* vector and plane are perpendicular, assume that it lies exactly on it */
return false;
}
static bool find_closest_face(uint8_t* object_result, uint16_t *face_result) {
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
uint8_t closest_obj = INVALID_OBJECT;
uint16_t closest_face = INVALID_FACE;
float closest_distance = INFINITY;
for (uint8_t oi = 0; oi < state.objects_sz; ++oi) {
Object *o = &state.objects[oi];
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[1] == INVALID_POINT) continue;
Vec3 p0 = point_to_vec3(oi, f->p[0]);
Vec3 p1 = point_to_vec3(oi, f->p[1]);
Vec3 p2 = point_to_vec3(oi, f->p[2]);
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
/* culling */
if (vec3_dot(state.camera_direction, n) >= 0) continue;
Vec3 i;
if (!vector_plane_intersection(cam.position, cam.direction, p0, n, &i))
continue;
float dist = vec3_length(vec3_sub(p0, i));
if (dist >= closest_distance) continue;
/* left normals are used to determine whether point lies to the left for all forming lines */
Vec3 ln0 = vec3_norm(vec3_cross(n, vec3_sub(p1, p0)));
if (vec3_dot(ln0, vec3_sub(p0, i)) >= 0) continue;
Vec3 ln1 = vec3_norm(vec3_cross(n, vec3_sub(p2, p1)));
if (vec3_dot(ln1, vec3_sub(p1, i)) >= 0) continue;
if (f->p[3] == INVALID_POINT) {
/* triangle */
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p0, p2)));
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
} else {
/* quad */
Vec3 p3 = point_to_vec3(oi, f->p[3]);
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p3, p2)));
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
Vec3 ln3 = vec3_norm(vec3_cross(n, vec3_sub(p0, p3)));
if (vec3_dot(ln3, vec3_sub(p3, i)) >= 0) continue;
}
closest_distance = dist;
closest_face = fi;
closest_obj = oi;
}
}
if (closest_face == INVALID_FACE)
return false;
if (object_result)
*object_result = closest_obj;
if (face_result)
*face_result = closest_face;
return true;
}
static void show_snap_lines(Vec3 p) {
float step = 1.0f / ((float)POINTS_PER_METER / state.grid_snap_granularity);
int const lines_per_side = (SNAP_LINES_SHOW - 1) / 2;
for (int l = -lines_per_side; l <= lines_per_side; ++l) {
if (!state.axis_mask[0]) {
Vec3 c = vec3_add(p, vec3_scale((Vec3){1,0,0}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale((Vec3){0,0,1}, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale((Vec3){0,0,1}, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
if (!state.axis_mask[1]) {
Vec3 axis = fabsf(vec3_dot(state.camera_direction, (Vec3){0,0,1})) >= 0.5f ? (Vec3){1,0,0} : (Vec3){0,0,1};
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,1,0}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale(axis, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale(axis, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
if (!state.axis_mask[2]) {
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,0,1}, step * (float)l));
draw_line_3d(
vec3_add(c, vec3_scale((Vec3){1,0,0}, SNAP_LINES_WIDTH)),
vec3_add(c, vec3_scale((Vec3){1,0,0}, -SNAP_LINES_WIDTH)),
1,
SNAP_LINES_COLOR);
}
}
}
static bool process_operation_move_point(Operation *op) {
/* finish dragging around */
/* TODO: dont keep empty ops on stack? */
if (input_action_just_released("select")) {
state.op_active = false;
return false;
}
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
draw_billboard("/data/grab.png",
point_to_vec3(op->data.move_point.object, op->data.move_point.point),
(Vec2){size, size},
(Rect){0},
(Color){255,255,255,255},
false);
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
bool point_moved = false;
/* figure out which planes are angled acutely against the viewing direction */
bool y_closer_than_horizon = fabsf(vec3_dot(state.camera_direction, (Vec3){0,1,0})) >= 0.5f;
Vec3 plane_normal_x = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){0,0,1};
Vec3 plane_normal_z = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){1,0,0};
/* show snapping in lines */
show_snap_lines(p);
Vec3 s;
if (!state.axis_mask[0]) {
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_x, &s)) {
int16_t xch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){1,0,0}) * (float)POINTS_PER_METER));
xch -= xch % state.grid_snap_granularity;
state.points[op->data.move_point.point].x += xch;
op->data.move_point.delta_x += xch;
if (xch != 0) point_moved = true;
}
}
if (!state.axis_mask[1]) {
if (vector_plane_intersection(cam.position, cam.direction, p, (Vec3){0,0,1}, &s)) {
int16_t ych = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,1,0}) * (float)POINTS_PER_METER));
ych -= ych % state.grid_snap_granularity;
state.points[op->data.move_point.point].y += ych;
op->data.move_point.delta_y += ych;
if (ych != 0) point_moved = true;
}
}
if (!state.axis_mask[2]) {
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_z, &s)) {
int16_t zch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,0,1}) * (float)POINTS_PER_METER));
zch -= zch % state.grid_snap_granularity;
state.points[op->data.move_point.point].z += zch;
op->data.move_point.delta_z += zch;
if (zch != 0) point_moved = true;
}
}
if (point_moved) {
audio_play("/data/bong.ogg", NULL, false, 0.12f, 0.0f);
return true;
}
return false;
}
static void reverse_operation_move_point(Operation *op) {
SDL_assert(op->kind == OPERATION_MOVE_POINT);
state.points[op->data.move_point.point].x -= op->data.move_point.delta_x;
state.points[op->data.move_point.point].y -= op->data.move_point.delta_y;
state.points[op->data.move_point.point].z -= op->data.move_point.delta_z;
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
}
static void reverse_operation_set_texture(Operation *op) {
SDL_assert(op->kind == OPERATION_SET_TEXTURE);
Object *o = &state.objects[op->data.set_texture.object];
o->faces[op->data.set_texture.face].texture += op->data.set_texture.delta_texture;
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
}
static void reverse_triangulation(Operation *op) {
SDL_assert(op->kind == OPERATION_TRIANGULATE);
Object *o = &state.objects[op->data.set_texture.object];
Face *fn = &o->faces[op->data.triangulate.new_face];
Face *fo = &o->faces[op->data.triangulate.old_face];
fo->p[3] = fo->p[2];
fo->p[2] = fn->p[1];
pop_face(op->data.set_texture.object, op->data.triangulate.new_face);
}
/* TODO: reverse of this */
static void try_subdividing_from_moving(uint8_t object, uint16_t point) {
Object *o = &state.objects[object];
bool not_first = false;
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
if (f->p[3] == INVALID_POINT) continue;
for (uint16_t pi = 0; pi < 4; ++pi) {
if (f->p[pi] == point) {
Face new0 = *f;
new0.p[0] = f->p[pi];
new0.p[1] = f->p[(pi + 1) % 4];
new0.p[2] = f->p[(pi + 3) % 4];
new0.p[3] = INVALID_POINT;
uint16_t newf = push_face(
object,
f->p[(pi + 1) % 4],
f->p[(pi + 2) % 4],
f->p[(pi + 3) % 4],
INVALID_POINT,
f->texture,
f->tex_scale,
f->tex_x,
f->tex_y);
*f = new0;
extend_operation((Operation){
.kind = OPERATION_TRIANGULATE,
.data = { .triangulate = { .new_face = newf, .old_face = fi, .object = object} },
.chained = not_first,
});
not_first = true;
}
}
}
}
static void process_undo(void) {
if (state.op_active)
state.op_active = false;
/* TODO: checks and defined limit */
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
state.op_stack_ptr--;
switch (op->kind) {
case OPERATION_MOVE_POINT: {
reverse_operation_move_point(op);
break;
}
case OPERATION_SET_TEXTURE: {
reverse_operation_set_texture(op);
break;
}
case OPERATION_TRIANGULATE: {
reverse_triangulation(op);
break;
}
default:
(void)0;
}
/* pop another if they're chained together */
if (op->chained) process_undo();
}
static void process_operations(void) {
if (input_action_just_pressed("undo"))
process_undo();
if (!state.op_active) {
/* point dragging */
if (!state.current_texture) {
uint16_t point_select; uint8_t obj_select;
if (find_closest_point(&obj_select, &point_select)) {
draw_billboard("/data/point.png",
point_to_vec3(obj_select, point_select),
(Vec2){0.05f, 0.05f},
(Rect){0},
(Color){255,255,255,255},
false);
if (input_action_just_pressed("select"))
push_operation((Operation){
.kind = OPERATION_MOVE_POINT,
.data = { .move_point = { .point = point_select, .object = obj_select } },
}, true );
}
/* texture setting */
} else {
uint8_t obj_select; uint16_t face_select;
if (find_closest_face(&obj_select, &face_select)) {
state.current_hovered_face = face_select;
state.current_hovered_obj = obj_select;
if (input_action_just_pressed("rotate")) {
Face *f = &state.objects[obj_select].faces[face_select];
int16_t tex_x = f->tex_x;
int16_t tex_y = f->tex_y;
f->tex_x = -tex_y;
f->tex_y = tex_x;
}
if (input_action_pressed("select") && state.current_texture) {
uint8_t new_tex = push_texture(obj_select, state.current_texture);
uint8_t cur_tex = state.objects[obj_select].faces[face_select].texture;
if (new_tex != cur_tex) {
state.objects[obj_select].faces[face_select].texture = new_tex;
audio_play("/data/bong.ogg", NULL, false, 0.5f, 0.0f);
push_operation((Operation){
.kind = OPERATION_SET_TEXTURE,
.data = { .set_texture = {
.face = face_select,
.object = obj_select,
.delta_texture = cur_tex - new_tex,
}},
}, false );
}
}
}
}
}
if (state.op_active) {
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
switch (op->kind) {
case OPERATION_MOVE_POINT: {
bool update = process_operation_move_point(op);
if (update)
try_subdividing_from_moving(op->data.move_point.object, op->data.move_point.point);
break;
}
case OPERATION_SET_TEXTURE:
case OPERATION_TRIANGULATE:
default:
(void)0;
}
}
}
static void draw_axes(void) {
/* axis helpers */
/* idea: double selection of axes for diagonal edits */
draw_line_3d(state.active_center, (Vec3){256,0,0}, 1, state.axis_mask[0] ? (Color){0,0,0,75} : (Color){255,0,0,125});
draw_billboard("/data/x.png",
vec3_add(state.active_center, (Vec3){2,0,0}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[0] ? (Color){0,0,0,255} : (Color){255,0,0,255},
false);
draw_line_3d(state.active_center, (Vec3){0,256,0}, 1, state.axis_mask[1] ? (Color){0,0,0,75} : (Color){75,125,25,125});
draw_billboard("/data/y.png",
vec3_add(state.active_center, (Vec3){0,1.5f,0}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[1] ? (Color){0,0,0,255} : (Color){75,125,25,255},
false);
draw_line_3d(state.active_center, (Vec3){0,0,256}, 1, state.axis_mask[2] ? (Color){0,0,0,75} : (Color){0,0,255,125});
draw_billboard("/data/z.png",
vec3_add(state.active_center, (Vec3){0,0,2}),
(Vec2){0.1f, 0.1f},
(Rect){0},
state.axis_mask[2] ? (Color){0,0,0,255} : (Color){0,0,255,255},
false);
}
static void draw_hovered_face_border(void) {
if (state.current_hovered_obj == INVALID_OBJECT || state.current_hovered_face == INVALID_FACE)
return;
Object *o = &state.objects[state.current_hovered_obj];
Face *f = &o->faces[state.current_hovered_face];
if (f->p[3] != INVALID_POINT) {
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
Vec3 p3 = point_to_vec3(state.current_hovered_obj, f->p[3]);
Vec3 center = vec3_scale(vec3_add(p2, p0), 0.5);
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
Vec3 c3 = vec3_add(p3, vec3_scale(vec3_sub(p3, center), size));
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
draw_line_3d(c2, c3, 1, (Color){255,255,255,255});
draw_line_3d(c3, c0, 1, (Color){255,255,255,255});
} else {
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
Vec3 center = vec3_scale(vec3_add(p0, vec3_add(p1, p2)), 0.33f);
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
draw_line_3d(c2, c0, 1, (Color){255,255,255,255});
}
}
static void display_texture_selection(void) {
String list = file_read("/data/assets/", ":images");
if (!list.data)
return;
draw_rectangle((Rect){0, 480 - 50, 640, 480}, (Color){0,0,0,126});
char *selected = NULL;
char *saveptr = NULL;
int count = 0;
char const *part = SDL_strtokr(list.data, "\n", &saveptr);
do {
Rect box = (Rect){(float)count * 50, 480 - 48, 48, 48};
draw_sprite(part,
box,
(Rect){0,0,64,64},
(Color){255,255,255,255},
0, false, false, true);
count++;
if (state.current_texture && SDL_strcmp(part, state.current_texture) == 0)
draw_box(box, 1, (Color){255,255,255,255});
if (rect_intersects(box, (Rect){ctx.mouse_position.x, ctx.mouse_position.y, 1, 1}))
selected = SDL_strdup(part);
} while ((part = SDL_strtokr(NULL, "\n", &saveptr)));
if (selected) {
draw_text(selected, (Vec2){0, 480 - 48 - 24}, 24, (Color){255,255,255,255}, NULL);
if (input_action_just_pressed("select")) {
if (state.current_texture) SDL_free(state.current_texture);
state.current_texture = selected;
} else
SDL_free(selected);
}
}
static void process_camera_inputs(void) {
if (input_action_just_pressed("toggle_display_mode")) {
audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
state.solid_display_mode = !state.solid_display_mode;
if (state.current_texture) {
SDL_free(state.current_texture);
state.current_texture = NULL;
}
}
if (input_action_just_pressed("toggle_projection")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = !state.camera_is_orthographic;
}
if (input_action_just_pressed("toggle_x_axis")) {
state.axis_mask[0] = 0; state.axis_mask[1] = 1; state.axis_mask[2] = 1;
}
if (input_action_just_pressed("toggle_y_axis")) {
state.axis_mask[0] = 1; state.axis_mask[1] = 0; state.axis_mask[2] = 1;
}
if (input_action_just_pressed("toggle_z_axis")) {
state.axis_mask[0] = 1; state.axis_mask[1] = 1; state.axis_mask[2] = 0;
}
/* TODO: determine bounding box for this? */
/* TODO: no idea whether it's all correct in terms of directions. */
if (input_action_just_pressed("camera_front")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,0,-1};
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,2});
}
if (input_action_just_pressed("camera_back")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,0,1};
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,-2});
}
if (input_action_just_pressed("camera_left")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){1,0,0};
state.camera_position = vec3_add(state.active_center, (Vec3){-2,0,0});
}
if (input_action_just_pressed("camera_right")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){-1,0,0};
state.camera_position = vec3_add(state.active_center, (Vec3){2,0,0});
}
/* TODO: broken */
if (input_action_just_pressed("camera_above")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,-1,0};
state.camera_position = vec3_add(state.active_center, (Vec3){0,2,0});
}
/* TODO: broken */
if (input_action_just_pressed("camera_below")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = true;
state.camera_direction = (Vec3){0,1,0};
state.camera_position = vec3_add(state.active_center, (Vec3){0,-2,0});
}
}
void game_tick(void) {
if (!init) {
/* default state */
new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
state.camera_position = (Vec3){2,1,2};
state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
state.camera_zoom = 0.5f;
state.grid_snap_granularity = 16;
state.axis_mask[1] = 1; state.axis_mask[2] = 1;
init = true;
}
state.current_hovered_face = INVALID_FACE;
state.current_hovered_obj = INVALID_OBJECT;
input_action("toggle_display_mode", "Q");
input_action("toggle_projection", "TAB");
input_action("toggle_x_axis", "Z");
input_action("toggle_y_axis", "X");
input_action("toggle_z_axis", "C");
input_action("select", "LCLICK");
input_action("rotate", "R");
input_action("undo", "F");
input_action("camera_front", "KP0");
input_action("camera_back", "KP1");
input_action("camera_left", "KP2");
input_action("camera_right", "KP3");
input_action("camera_above", "KP4");
input_action("camera_below", "KP5");
process_camera_inputs();
process_camera_movement();
process_operations();
/* helpres */
draw_axes();
draw_hovered_face_border();
for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
render_object(obj);
if (state.solid_display_mode)
display_texture_selection();
draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
draw_camera(
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100
);
}
void game_end(void) {
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
for (uint8_t t = 0; t < o->textures_sz; ++t)
SDL_free(o->textures[t]);
SDL_free(o->name);
}
if (state.current_texture) {
SDL_free(state.current_texture);
state.current_texture = NULL;
}
}

View File

@ -1 +1,2 @@
luabind.c luabind.c
data/scripts/twnapi.lua

View File

@ -1,42 +1,36 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.30)
cmake_policy(SET CMP0171 NEW)
project(twnlua LANGUAGES C) project(twnlua LANGUAGES C)
find_package(Python3 COMPONENTS Interpreter)
if(NOT CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug) set(CMAKE_BUILD_TYPE Debug)
endif() endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build) add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
set(FLAGS
$<$<NOT:$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>>:--no-dynlib-game>
)
add_custom_command( add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/game.c
COMMAND ${PYTHON3} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json ${FLAGS} > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json ${FLAGS} > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json
CODEGEN
) )
add_custom_command( add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/data/scripts/twnapi.lua OUTPUT ${TWN_OUT_DIR}/data/scripts/twnapi.lua
COMMAND ${PYTHON3} ${CMAKE_CURRENT_SOURCE_DIR}/docgen.py $ENV{TWNROOT}/share/twn_api.json > ${CMAKE_SOURCE_DIR}/data/scripts/twnapi.lua COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/docgen.py $ENV{TWNROOT}/share/twn_api.json > ${TWN_OUT_DIR}/data/scripts/twnapi.lua
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/docgen.py $ENV{TWNROOT}/share/twn_api.json DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/docgen.py $ENV{TWNROOT}/share/twn_api.json
) )
add_custom_target( add_custom_target(
twnlua_docgen ALL twnlua_docgen ALL
DEPENDS ${CMAKE_SOURCE_DIR}/data/scripts/twnapi.lua DEPENDS ${TWN_OUT_DIR}/data/scripts/twnapi.lua
) )
add_compile_definitions(LUA_32BITS) add_compile_definitions(LUA_32BITS=1)
set(SOURCE_FILES set(SOURCE_FILES
game.c game.c
state.h
minilua.c minilua.c
${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
) )
use_townengine(${CMAKE_PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_SOURCE_DIR}) use_townengine("${SOURCE_FILES}" ${TWN_OUT_DIR})

View File

@ -36,7 +36,7 @@ def to_table(typedesc, variable, indent = 0):
binding = ' ' * indent + "lua_createtable(L, 0, %i);\n" % len(typedesc["fields"]) binding = ' ' * indent + "lua_createtable(L, 0, %i);\n" % len(typedesc["fields"])
for field in typedesc["fields"]: for field in typedesc["fields"]:
if field["type"] == "float" or field["type"] == "uint8_t": if field["type"] == "float" or field["type"] == "uint8_t":
binding += ' ' * indent + "lua_pushnumber(L, (double)(%s));\n" % (variable + ".%s" % field["name"]) binding += ' ' * indent + "lua_pushnumber(L, (float)(%s));\n" % (variable + ".%s" % field["name"])
elif field["type"] == "bool": elif field["type"] == "bool":
binding += ' ' * indent + "lua_pushboolean(L, (%s));\n" % (variable + ".%s" % field["name"]) binding += ' ' * indent + "lua_pushboolean(L, (%s));\n" % (variable + ".%s" % field["name"])
elif field["type"] in api["types"]: elif field["type"] in api["types"]:
@ -64,10 +64,7 @@ def from_table(typedesc, variable, indent = 0):
print('#include "twn_game_api.h"\n') print('#include "twn_game_api.h"\n')
if not "--no-dynlib-game" in sys.argv: print('/* assumed to be included from game.c, where minilua.h is already present */\n')
print('#define STB_DS_IMPLEMENTATION')
print('#include <stb_ds.h>')
print('#include "minilua.h"\n')
bindings, used_converters = [], {} bindings, used_converters = [], {}
for procedure, procedure_desc in api["procedures"].items(): for procedure, procedure_desc in api["procedures"].items():
@ -112,11 +109,13 @@ for procedure, procedure_desc in api["procedures"].items():
if procedure_desc["return"] == "bool": if procedure_desc["return"] == "bool":
binding += " lua_pushboolean(L, (int)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " lua_pushboolean(L, (int)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "float": elif procedure_desc["return"] == "float":
binding += " lua_pushnumber(L, (double)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " lua_pushnumber(L, (float)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "char *": elif procedure_desc["return"] == "char *":
binding += " lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "String":
binding += " String result = %s(%s);\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
binding += " lua_pushlstring(L, result.data, (int)result.length);\n"
elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]: elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]:
# TODO: handle enums
type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]] type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]]
binding += " %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"])) binding += " %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
binding += to_table(type_desc, "result", 4) binding += to_table(type_desc, "result", 4)
@ -150,35 +149,17 @@ for typename, typedesc in used_converters.items():
raise BaseException("Unhandled converter field type '%s'" % (field["type"])) raise BaseException("Unhandled converter field type '%s'" % (field["type"]))
converter += " lua_pop(L, %i);\n" % len(typedesc["fields"]); converter += " lua_pop(L, %i);\n" % len(typedesc["fields"]);
# TODO: wild idea: use compile time built hash table
elif "enums" in typedesc:
storages += ["struct %sHashItem { char *key; %s value; };\nstatic struct %sHashItem *%s_map = NULL;\n" % (typename, typename, typename, typename.lower())]
# TODO: use arena
for enum in typedesc["enums"]:
initializer = " shput(%s_map, \"%s\", %s);" % (typename.lower(), enum, typedesc["enums"][enum])
initializers += [initializer]
deinitializers += [" shfree(%s_map);" % typename.lower()]
converter += " char const *value = lua_tostring(L, -1);\n";
converter += " %s = shget(%s_map, value);\n" % (typename.lower(), typename.lower())
converter += " lua_pop(L, 1);\n";
converter += " return %s;\n}\n" % (typename.lower()) converter += " return %s;\n}\n" % (typename.lower())
converters += [converter] converters += [converter]
print('\n'.join(storages)) print('\n'.join(storages))
print("extern void bindgen_init(void);\n")
print("void bindgen_init(void) {\n" + '\n'.join(initializers) + "\n}\n")
print('\n'.join(converters)) print('\n'.join(converters))
print('\n'.join(bindings)) print('\n'.join(bindings))
loader = "extern void bindgen_load_%s(lua_State *L);\n" % api["name"] loader = "extern void bindgen_load_%s(lua_State *L);\n" % api["name"]
loader += "void bindgen_load_%s(lua_State *L) {\n" % api["name"] loader += "void bindgen_load_%s(lua_State *L) {\n" % api["name"]
loader += " bindgen_init();\n"
for procedure, procedure_desc in api["procedures"].items(): for procedure, procedure_desc in api["procedures"].items():
loader += " lua_pushcfunction(L, binding_%s);\n" % procedure loader += " lua_pushcfunction(L, binding_%s);\n" % procedure
loader += " lua_setglobal(L, \"%s\");\n" % procedure loader += " lua_setglobal(L, \"%s\");\n" % procedure

View File

@ -1,3 +1,3 @@
[[deps]] [[deps]]
source = "../../common-data" # where does it come from, might be an url source = "../../data" # where does it come from, might be an url
name = "common-data" # should be globally unique name = "common-data" # should be globally unique

View File

@ -4,16 +4,39 @@ offset = { x = 0, y = 0 }
angle = 0 angle = 0
function game_tick() function game_tick()
if ctx.udata == nil then
ctx.udata = {
frame_count = 0,
nest = {
frame_count = 0,
},
arr = { [0] = 0 },
}
end
draw_text {
string = tostring(ctx.udata.frame_count),
position = { x = 0, y = 0 },
font = "/fonts/kenney-pixel.ttf",
}
draw_text {
string = tostring(ctx.udata.nest.frame_count),
position = { x = 0, y = 14 },
font = "/fonts/kenney-pixel.ttf",
}
draw_text {
string = tostring(ctx.udata.arr[0]),
position = { x = 0, y = 28 },
font = "/fonts/kenney-pixel.ttf",
}
input_action { input_action {
name = "press", name = "press",
control = "A" control = "A"
} }
draw_rectangle {
rect = { x = 0, y = 0, w = 640, h = 360 },
color = { r = 127, g = 0, b = 127, a = 255 },
}
draw_sprite { draw_sprite {
texture = "/assets/title.png", texture = "/assets/title.png",
rect = { rect = {
@ -32,6 +55,10 @@ function game_tick()
} }
end end
ctx.udata.frame_count = ctx.udata.frame_count + 1
ctx.udata.nest.frame_count = ctx.udata.nest.frame_count + 1
ctx.udata.arr[0] = ctx.udata.arr[0] + 1
offset.x = ORIGIN.x + (math.cos(angle) * RADIUS) offset.x = ORIGIN.x + (math.cos(angle) * RADIUS)
offset.y = ORIGIN.y + (math.sin(angle) * RADIUS) offset.y = ORIGIN.y + (math.sin(angle) * RADIUS)
angle = angle + 0.1 angle = angle + 0.1

View File

@ -6,5 +6,6 @@ dev_id = "somebody"
[game] [game]
resolution = [ 640, 360 ] resolution = [ 640, 360 ]
background_color = [ 127, 0, 127, 255 ]
[engine] [engine]

View File

@ -7,16 +7,16 @@ with open(sys.argv[1], 'r') if sys.argv[1] != "-" else sys.stdin as f:
api = json.loads(api_source) api = json.loads(api_source)
def to_lua_type_annot(typename): def to_lua_type_annot(typedesc):
basetype = typename.rsplit(' *', 1)[0] if type(typedesc) is dict:
if typename == "char *": return r'{ %s }' % ','.join('%s: %s' % (f["name"], to_lua_type_annot(f["type"])) for f in typedesc["fields"])
basetype = typedesc.rsplit(' *', 1)[0]
if typedesc == "char *":
return "string" return "string"
elif basetype == "float": elif basetype == "float":
return "number" return "number"
elif basetype == "bool": elif basetype == "bool":
return "boolean" return "boolean"
elif basetype == "Control":
return "Control"
elif basetype == "Vec2": elif basetype == "Vec2":
return r"{ x: number, y: number }" return r"{ x: number, y: number }"
elif basetype == "Vec3": elif basetype == "Vec3":
@ -27,9 +27,12 @@ def to_lua_type_annot(typename):
return r"{ x: number, y: number, w: number, h: number }" return r"{ x: number, y: number, w: number, h: number }"
else: else:
return "unknown" return "unknown"
# raise BaseException("Unhandled type for annotation: %s" % typename) # raise BaseException("Unhandled type for annotation: %s" % typedesc)
type_annotations, enum_annotations = {}, {} print("---@meta twn")
print("---@diagnostic disable")
type_annotations = {}
type_annotations["ctx"] = r"{ %s, udata: table }" % \ type_annotations["ctx"] = r"{ %s, udata: table }" % \
', '.join("%s: %s" % (f["name"], to_lua_type_annot(f["type"])) for f in api["types"]["Context"]["fields"]) ', '.join("%s: %s" % (f["name"], to_lua_type_annot(f["type"])) for f in api["types"]["Context"]["fields"])
@ -37,17 +40,16 @@ for annot in type_annotations:
print("---@type " + type_annotations[annot]) print("---@type " + type_annotations[annot])
print(r"%s = nil" % annot) print(r"%s = nil" % annot)
enum_annotations["Control"] = \
'|'.join('\'"%s"\'' % e for e in api["types"]["Control"]["enums"])
for annot in enum_annotations:
print("---@alias %s %s" % (annot, enum_annotations[annot]))
procedure_annotations = {} procedure_annotations = {}
for procedure, procedure_desc in api["procedures"].items(): for procedure, procedure_desc in api["procedures"].items():
procedure_annotations[procedure] = r"{ %s }" % \ procedure_annotations[procedure] = {}
procedure_annotations[procedure]["params"] = r"{ %s }" % \
', '.join("%s: %s" % (p["name"], to_lua_type_annot(p["type"]) + '?' * ("default" in p)) for p in procedure_desc["params"]) ', '.join("%s: %s" % (p["name"], to_lua_type_annot(p["type"]) + '?' * ("default" in p)) for p in procedure_desc["params"])
if "return" in procedure_desc:
procedure_annotations[procedure]["return"] = to_lua_type_annot(procedure_desc["return"])
for annot in procedure_annotations: for annot in procedure_annotations:
print("---@param args " + procedure_annotations[annot]) print("---@param args " + procedure_annotations[annot]["params"])
if "return" in procedure_annotations[annot]:
print("---@return " + procedure_annotations[annot]["return"])
print("function %s(args) end" % annot) print("function %s(args) end" % annot)

View File

@ -1,9 +1,15 @@
#include "twn_game_api.h" #include "twn_game_api.h"
#include "state.h"
/* TODO: actually move it back it its own file, it doesn't give any compilation benefits */
#include "minilua.h" #include "minilua.h"
#include "state.h"
#include "luabind.c"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#define UDATA_NESTING_LIMIT 128
/* generated by bindgen.py */ /* generated by bindgen.py */
void bindgen_load_twn(lua_State *L); void bindgen_load_twn(lua_State *L);
@ -14,30 +20,48 @@ void bindgen_upload_context(lua_State *L);
/* require will go through physicsfs exclusively so that scripts can be in the data dir */ /* require will go through physicsfs exclusively so that scripts can be in the data dir */
/* TODO: allow for bytecode files */ /* TODO: allow for bytecode files */
/* TODO: support .lua suffixes files? */
static int physfs_loader(lua_State *L) { static int physfs_loader(lua_State *L) {
const char *name = luaL_checkstring(L, 1); const char *name = luaL_checkstring(L, 1);
char *final_path = NULL;
SDL_asprintf(&final_path, "/scripts/%s.lua", name);
if (!file_exists(final_path)) { static const char *name_breaker = NULL;
if (name_breaker && SDL_strcmp(name, name_breaker) == 0) {
log_string(name_breaker, "Recursive load on itself from lua module");
return 0;
} name_breaker = name;
/* replace dots with path slashes */
char *path_copy = SDL_strdup(name);
char *ch = NULL;
while ((ch = SDL_strchr(path_copy, '.')))
*ch = '/';
char *final_path = NULL;
SDL_asprintf(&final_path, "/scripts/%s.lua", path_copy);
SDL_free(path_copy);
if (SDL_strcmp(file_read(final_path, ":exists").data, "yes") == 0) {
char *error_message = NULL; char *error_message = NULL;
SDL_asprintf(&error_message, "could not find module %s in filesystem", name); SDL_asprintf(&error_message, "could not find module %s in filesystem", name);
lua_pushstring(L, error_message); lua_pushstring(L, error_message);
free(error_message); SDL_free(error_message);
free(final_path); SDL_free(final_path);
return 1; return 1;
} }
unsigned char *buf = NULL; char *final_path_binary = NULL;
int64_t buf_size = file_to_bytes(final_path, &buf); SDL_asprintf(&final_path_binary, "%s%s", final_path, ":binary");
free(final_path);
String file = file_read(final_path, ":binary");
SDL_free(final_path);
/* TODO: use reader interface for streaming instead */ /* TODO: use reader interface for streaming instead */
luaL_loadbuffer(L, (char *)buf, buf_size, name); int const result = luaL_loadbuffer(L, file.data, (size_t)file.length, name);
free(buf); if (result != LUA_OK)
log_string(lua_tostring(L, -1), NULL);
return 1; return result == LUA_OK;
} }
@ -84,21 +108,76 @@ static void *custom_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
} }
static void exchange_lua_states(lua_State *from, lua_State *to, int level, int index) {
if (level >= UDATA_NESTING_LIMIT) {
log_string("ctx.udata nesting limit is reached", NULL);
return;
}
/* TODO: use arrays for optimized paths */
/* TODO: preallocate table records */
switch (lua_type(from, index)) {
case LUA_TTABLE:
lua_newtable(to);
lua_pushnil(from); /* first key */
while (lua_next(from, index - 1) != 0) {
/* 'key' at index -2 and 'value' at index -1 */
exchange_lua_states(from, to, level + 1, -2);
exchange_lua_states(from, to, level + 1, -1);
lua_settable(to, index - 2);
/* removes 'value'; keeps 'key' for next iteration */
lua_pop(from, 1);
}
break;
case LUA_TNUMBER:
lua_pushnumber(to, lua_tonumber(from, index));
break;
case LUA_TBOOLEAN:
lua_pushboolean(to, lua_toboolean(from, index));
break;
case LUA_TSTRING:
lua_pushstring(to, lua_tostring(from, index));
break;
case LUA_TNIL:
lua_pushnil(to);
break;
default:
/* TODO: provide a path and type of it for better diagnostic */
log_string("Unserializable udata found and is ignored", NULL);
break;
}
}
void game_tick(void) { void game_tick(void) {
if (ctx.initialization_needed) { if (ctx.initialization_needed) {
if (!ctx.udata) if (!ctx.udata)
ctx.udata = ccalloc(1, sizeof (State)); ctx.udata = SDL_calloc(1, sizeof (State));
State *state = ctx.udata; State *state = ctx.udata;
/* let's init lua */ /* let's init lua */
/* state existed already */ lua_State *new_state = lua_newstate(custom_alloc, NULL);
lua_setallocf(new_state, custom_alloc, NULL);
/* state existed already, copy its udata over */
if (state->L != NULL) { if (state->L != NULL) {
lua_getglobal(state->L, "ctx");
lua_getfield(state->L, -1, "udata");
SDL_assert(!lua_isnoneornil(state->L, -1));
SDL_assert(!lua_isnoneornil(state->L, -2));
if (!lua_isnoneornil(state->L, -1)) {
lua_newtable(new_state);
exchange_lua_states(state->L, new_state, 0, -1);
lua_setfield(new_state, -2, "udata");
lua_setglobal(new_state, "ctx");
}
/* bye :) */
lua_close(state->L); lua_close(state->L);
} }
state->L = luaL_newstate();
lua_setallocf(state->L, custom_alloc, NULL); state->L = new_state;
/* fakey version of luaL_openlibs() that excludes file i/o and os stuff */ /* fakey version of luaL_openlibs() that excludes file i/o and os stuff */
{ {
@ -136,24 +215,21 @@ void game_tick(void) {
bindgen_load_twn(state->L); bindgen_load_twn(state->L);
/* now finally get to running the code */ /* now finally get to running the code */
unsigned char *game_buf = NULL; String file = file_read("/scripts/game.lua", ":binary");
size_t game_buf_size = file_to_bytes("/scripts/game.lua", &game_buf);
/* TODO: use reader interface for streaming instead */ /* TODO: use reader interface for streaming instead */
if (luaL_loadbuffer(state->L, (char *)game_buf, game_buf_size, "game.lua") == LUA_OK) { if (luaL_loadbuffer(state->L, file.data, (size_t)file.length, "game.lua") == LUA_OK) {
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) { if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
log_critical("%s", lua_tostring(state->L, -1)); log_string(luaL_tolstring(state->L, -1, NULL), "Error executing /scripts/game.lua entry");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} else } else
state->loaded_successfully = true; state->loaded_successfully = true;
} else { } else {
/* got some sort of error, it should be pushed on top of the stack */ /* got some sort of error, it should be pushed on top of the stack */
SDL_assert(lua_isstring(state->L, -1)); SDL_assert(lua_isstring(state->L, -1));
log_critical("Error loading /scripts/game.lua entry: %s", luaL_tolstring(state->L, -1, NULL)); log_string(luaL_tolstring(state->L, -1, NULL), "Error loading /scripts/game.lua entry");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} }
free(game_buf);
/* from this point we have access to everything defined in lua */ /* from this point we have access to everything defined in lua */
} }
@ -171,7 +247,7 @@ void game_tick(void) {
lua_getglobal(state->L, "game_tick"); lua_getglobal(state->L, "game_tick");
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) { if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
log_critical("%s", lua_tostring(state->L, -1)); log_string(luaL_tolstring(state->L, -1, NULL), "Error executing game_tick()");
lua_pop(state->L, 1); lua_pop(state->L, 1);
} }
@ -185,5 +261,5 @@ void game_end(void) {
State *state = ctx.udata; State *state = ctx.udata;
bindgen_unload_twn(state->L); bindgen_unload_twn(state->L);
lua_close(state->L); lua_close(state->L);
free(state); SDL_free(state);
} }

View File

@ -210,8 +210,9 @@ extern "C" {
/* /*
@@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats. @@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats.
*/ */
#if !defined(LUA_32BITS)
#define LUA_32BITS 0 #define LUA_32BITS 0
#endif
/* /*
@@ LUA_C89_NUMBERS ensures that Lua uses the largest types available for @@ LUA_C89_NUMBERS ensures that Lua uses the largest types available for
@ -382,7 +383,8 @@ extern "C" {
#else /* }{ */ #else /* }{ */
#define LUA_API extern /* TWN: Don't export anything, there's no need. */
#define LUA_API LUAI_FUNC
#endif /* } */ #endif /* } */
@ -28631,696 +28633,6 @@ LUALIB_API void luaL_openlibs (lua_State *L) {
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#ifdef LUA_MAKE_LUA
/*
** $Id: lua.c $
** Lua stand-alone interpreter
** See Copyright Notice in lua.h
*/
#define lua_c
/*#include "lprefix.h"*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
/*#include "lua.h"*/
/*#include "lauxlib.h"*/
/*#include "lualib.h"*/
#if !defined(LUA_PROGNAME)
#define LUA_PROGNAME "lua"
#endif
#if !defined(LUA_INIT_VAR)
#define LUA_INIT_VAR "LUA_INIT"
#endif
#define LUA_INITVARVERSION LUA_INIT_VAR LUA_VERSUFFIX
static lua_State *globalL = NULL;
static const char *progname = LUA_PROGNAME;
#if defined(LUA_USE_POSIX) /* { */
/*
** Use 'sigaction' when available.
*/
static void setsignal (int sig, void (*handler)(int)) {
struct sigaction sa;
sa.sa_handler = handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask); /* do not mask any signal */
sigaction(sig, &sa, NULL);
}
#else /* }{ */
#define setsignal signal
#endif /* } */
/*
** Hook set by signal function to stop the interpreter.
*/
static void lstop (lua_State *L, lua_Debug *ar) {
(void)ar; /* unused arg. */
lua_sethook(L, NULL, 0, 0); /* reset hook */
luaL_error(L, "interrupted!");
}
/*
** Function to be called at a C signal. Because a C signal cannot
** just change a Lua state (as there is no proper synchronization),
** this function only sets a hook that, when called, will stop the
** interpreter.
*/
static void laction (int i) {
int flag = LUA_MASKCALL | LUA_MASKRET | LUA_MASKLINE | LUA_MASKCOUNT;
setsignal(i, SIG_DFL); /* if another SIGINT happens, terminate process */
lua_sethook(globalL, lstop, flag, 1);
}
static void print_usage (const char *badoption) {
lua_writestringerror("%s: ", progname);
if (badoption[1] == 'e' || badoption[1] == 'l')
lua_writestringerror("'%s' needs argument\n", badoption);
else
lua_writestringerror("unrecognized option '%s'\n", badoption);
lua_writestringerror(
"usage: %s [options] [script [args]]\n"
"Available options are:\n"
" -e stat execute string 'stat'\n"
" -i enter interactive mode after executing 'script'\n"
" -l mod require library 'mod' into global 'mod'\n"
" -l g=mod require library 'mod' into global 'g'\n"
" -v show version information\n"
" -E ignore environment variables\n"
" -W turn warnings on\n"
" -- stop handling options\n"
" - stop handling options and execute stdin\n"
,
progname);
}
/*
** Prints an error message, adding the program name in front of it
** (if present)
*/
static void l_message (const char *pname, const char *msg) {
if (pname) lua_writestringerror("%s: ", pname);
lua_writestringerror("%s\n", msg);
}
/*
** Check whether 'status' is not OK and, if so, prints the error
** message on the top of the stack.
*/
static int report (lua_State *L, int status) {
if (status != LUA_OK) {
const char *msg = lua_tostring(L, -1);
if (msg == NULL)
msg = "(error message not a string)";
l_message(progname, msg);
lua_pop(L, 1); /* remove message */
}
return status;
}
/*
** Message handler used to run all chunks
*/
static int msghandler (lua_State *L) {
const char *msg = lua_tostring(L, 1);
if (msg == NULL) { /* is error object not a string? */
if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */
lua_type(L, -1) == LUA_TSTRING) /* that produces a string? */
return 1; /* that is the message */
else
msg = lua_pushfstring(L, "(error object is a %s value)",
luaL_typename(L, 1));
}
luaL_traceback(L, L, msg, 1); /* append a standard traceback */
return 1; /* return the traceback */
}
/*
** Interface to 'lua_pcall', which sets appropriate message function
** and C-signal handler. Used to run all chunks.
*/
static int docall (lua_State *L, int narg, int nres) {
int status;
int base = lua_gettop(L) - narg; /* function index */
lua_pushcfunction(L, msghandler); /* push message handler */
lua_insert(L, base); /* put it under function and args */
globalL = L; /* to be available to 'laction' */
setsignal(SIGINT, laction); /* set C-signal handler */
status = lua_pcall(L, narg, nres, base);
setsignal(SIGINT, SIG_DFL); /* reset C-signal handler */
lua_remove(L, base); /* remove message handler from the stack */
return status;
}
static void print_version (void) {
lua_writestring(LUA_COPYRIGHT, strlen(LUA_COPYRIGHT));
lua_writeline();
}
/*
** Create the 'arg' table, which stores all arguments from the
** command line ('argv'). It should be aligned so that, at index 0,
** it has 'argv[script]', which is the script name. The arguments
** to the script (everything after 'script') go to positive indices;
** other arguments (before the script name) go to negative indices.
** If there is no script name, assume interpreter's name as base.
** (If there is no interpreter's name either, 'script' is -1, so
** table sizes are zero.)
*/
static void createargtable (lua_State *L, char **argv, int argc, int script) {
int i, narg;
narg = argc - (script + 1); /* number of positive indices */
lua_createtable(L, narg, script + 1);
for (i = 0; i < argc; i++) {
lua_pushstring(L, argv[i]);
lua_rawseti(L, -2, i - script);
}
lua_setglobal(L, "arg");
}
static int dochunk (lua_State *L, int status) {
if (status == LUA_OK) status = docall(L, 0, 0);
return report(L, status);
}
static int dofile (lua_State *L, const char *name) {
return dochunk(L, luaL_loadfile(L, name));
}
static int dostring (lua_State *L, const char *s, const char *name) {
return dochunk(L, luaL_loadbuffer(L, s, strlen(s), name));
}
/*
** Receives 'globname[=modname]' and runs 'globname = require(modname)'.
** If there is no explicit modname and globname contains a '-', cut
** the suffix after '-' (the "version") to make the global name.
*/
static int dolibrary (lua_State *L, char *globname) {
int status;
char *suffix = NULL;
char *modname = strchr(globname, '=');
if (modname == NULL) { /* no explicit name? */
modname = globname; /* module name is equal to global name */
suffix = strchr(modname, *LUA_IGMARK); /* look for a suffix mark */
}
else {
*modname = '\0'; /* global name ends here */
modname++; /* module name starts after the '=' */
}
lua_getglobal(L, "require");
lua_pushstring(L, modname);
status = docall(L, 1, 1); /* call 'require(modname)' */
if (status == LUA_OK) {
if (suffix != NULL) /* is there a suffix mark? */
*suffix = '\0'; /* remove suffix from global name */
lua_setglobal(L, globname); /* globname = require(modname) */
}
return report(L, status);
}
/*
** Push on the stack the contents of table 'arg' from 1 to #arg
*/
static int pushargs (lua_State *L) {
int i, n;
if (lua_getglobal(L, "arg") != LUA_TTABLE)
luaL_error(L, "'arg' is not a table");
n = (int)luaL_len(L, -1);
luaL_checkstack(L, n + 3, "too many arguments to script");
for (i = 1; i <= n; i++)
lua_rawgeti(L, -i, i);
lua_remove(L, -i); /* remove table from the stack */
return n;
}
static int handle_script (lua_State *L, char **argv) {
int status;
const char *fname = argv[0];
if (strcmp(fname, "-") == 0 && strcmp(argv[-1], "--") != 0)
fname = NULL; /* stdin */
status = luaL_loadfile(L, fname);
if (status == LUA_OK) {
int n = pushargs(L); /* push arguments to script */
status = docall(L, n, LUA_MULTRET);
}
return report(L, status);
}
/* bits of various argument indicators in 'args' */
#define has_error 1 /* bad option */
#define has_i 2 /* -i */
#define has_v 4 /* -v */
#define has_e 8 /* -e */
#define has_E 16 /* -E */
/*
** Traverses all arguments from 'argv', returning a mask with those
** needed before running any Lua code or an error code if it finds any
** invalid argument. In case of error, 'first' is the index of the bad
** argument. Otherwise, 'first' is -1 if there is no program name,
** 0 if there is no script name, or the index of the script name.
*/
static int collectargs (char **argv, int *first) {
int args = 0;
int i;
if (argv[0] != NULL) { /* is there a program name? */
if (argv[0][0]) /* not empty? */
progname = argv[0]; /* save it */
}
else { /* no program name */
*first = -1;
return 0;
}
for (i = 1; argv[i] != NULL; i++) { /* handle arguments */
*first = i;
if (argv[i][0] != '-') /* not an option? */
return args; /* stop handling options */
switch (argv[i][1]) { /* else check option */
case '-': /* '--' */
if (argv[i][2] != '\0') /* extra characters after '--'? */
return has_error; /* invalid option */
*first = i + 1;
return args;
case '\0': /* '-' */
return args; /* script "name" is '-' */
case 'E':
if (argv[i][2] != '\0') /* extra characters? */
return has_error; /* invalid option */
args |= has_E;
break;
case 'W':
if (argv[i][2] != '\0') /* extra characters? */
return has_error; /* invalid option */
break;
case 'i':
args |= has_i; /* (-i implies -v) *//* FALLTHROUGH */
case 'v':
if (argv[i][2] != '\0') /* extra characters? */
return has_error; /* invalid option */
args |= has_v;
break;
case 'e':
args |= has_e; /* FALLTHROUGH */
case 'l': /* both options need an argument */
if (argv[i][2] == '\0') { /* no concatenated argument? */
i++; /* try next 'argv' */
if (argv[i] == NULL || argv[i][0] == '-')
return has_error; /* no next argument or it is another option */
}
break;
default: /* invalid option */
return has_error;
}
}
*first = 0; /* no script name */
return args;
}
/*
** Processes options 'e' and 'l', which involve running Lua code, and
** 'W', which also affects the state.
** Returns 0 if some code raises an error.
*/
static int runargs (lua_State *L, char **argv, int n) {
int i;
for (i = 1; i < n; i++) {
int option = argv[i][1];
lua_assert(argv[i][0] == '-'); /* already checked */
switch (option) {
case 'e': case 'l': {
int status;
char *extra = argv[i] + 2; /* both options need an argument */
if (*extra == '\0') extra = argv[++i];
lua_assert(extra != NULL);
status = (option == 'e')
? dostring(L, extra, "=(command line)")
: dolibrary(L, extra);
if (status != LUA_OK) return 0;
break;
}
case 'W':
lua_warning(L, "@on", 0); /* warnings on */
break;
}
}
return 1;
}
static int handle_luainit (lua_State *L) {
const char *name = "=" LUA_INITVARVERSION;
const char *init = getenv(name + 1);
if (init == NULL) {
name = "=" LUA_INIT_VAR;
init = getenv(name + 1); /* try alternative name */
}
if (init == NULL) return LUA_OK;
else if (init[0] == '@')
return dofile(L, init+1);
else
return dostring(L, init, name);
}
/*
** {==================================================================
** Read-Eval-Print Loop (REPL)
** ===================================================================
*/
#if !defined(LUA_PROMPT)
#define LUA_PROMPT "> "
#define LUA_PROMPT2 ">> "
#endif
#if !defined(LUA_MAXINPUT)
#define LUA_MAXINPUT 512
#endif
/*
** lua_stdin_is_tty detects whether the standard input is a 'tty' (that
** is, whether we're running lua interactively).
*/
#if !defined(lua_stdin_is_tty) /* { */
#if defined(LUA_USE_POSIX) /* { */
#include <unistd.h>
#define lua_stdin_is_tty() isatty(0)
#elif defined(LUA_USE_WINDOWS) /* }{ */
#include <io.h>
#include <windows.h>
#define lua_stdin_is_tty() _isatty(_fileno(stdin))
#else /* }{ */
/* ISO C definition */
#define lua_stdin_is_tty() 1 /* assume stdin is a tty */
#endif /* } */
#endif /* } */
/*
** lua_readline defines how to show a prompt and then read a line from
** the standard input.
** lua_saveline defines how to "save" a read line in a "history".
** lua_freeline defines how to free a line read by lua_readline.
*/
#if !defined(lua_readline) /* { */
#if defined(LUA_USE_READLINE) /* { */
#include <readline/readline.h>
#include <readline/history.h>
#define lua_initreadline(L) ((void)L, rl_readline_name="lua")
#define lua_readline(L,b,p) ((void)L, ((b)=readline(p)) != NULL)
#define lua_saveline(L,line) ((void)L, add_history(line))
#define lua_freeline(L,b) ((void)L, free(b))
#else /* }{ */
#define lua_initreadline(L) ((void)L)
#define lua_readline(L,b,p) \
((void)L, fputs(p, stdout), fflush(stdout), /* show prompt */ \
fgets(b, LUA_MAXINPUT, stdin) != NULL) /* get line */
#define lua_saveline(L,line) { (void)L; (void)line; }
#define lua_freeline(L,b) { (void)L; (void)b; }
#endif /* } */
#endif /* } */
/*
** Return the string to be used as a prompt by the interpreter. Leave
** the string (or nil, if using the default value) on the stack, to keep
** it anchored.
*/
static const char *get_prompt (lua_State *L, int firstline) {
if (lua_getglobal(L, firstline ? "_PROMPT" : "_PROMPT2") == LUA_TNIL)
return (firstline ? LUA_PROMPT : LUA_PROMPT2); /* use the default */
else { /* apply 'tostring' over the value */
const char *p = luaL_tolstring(L, -1, NULL);
lua_remove(L, -2); /* remove original value */
return p;
}
}
/* mark in error messages for incomplete statements */
#define EOFMARK "<eof>"
#define marklen (sizeof(EOFMARK)/sizeof(char) - 1)
/*
** Check whether 'status' signals a syntax error and the error
** message at the top of the stack ends with the above mark for
** incomplete statements.
*/
static int incomplete (lua_State *L, int status) {
if (status == LUA_ERRSYNTAX) {
size_t lmsg;
const char *msg = lua_tolstring(L, -1, &lmsg);
if (lmsg >= marklen && strcmp(msg + lmsg - marklen, EOFMARK) == 0) {
lua_pop(L, 1);
return 1;
}
}
return 0; /* else... */
}
/*
** Prompt the user, read a line, and push it into the Lua stack.
*/
static int pushline (lua_State *L, int firstline) {
char buffer[LUA_MAXINPUT];
char *b = buffer;
size_t l;
const char *prmt = get_prompt(L, firstline);
int readstatus = lua_readline(L, b, prmt);
if (readstatus == 0)
return 0; /* no input (prompt will be popped by caller) */
lua_pop(L, 1); /* remove prompt */
l = strlen(b);
if (l > 0 && b[l-1] == '\n') /* line ends with newline? */
b[--l] = '\0'; /* remove it */
if (firstline && b[0] == '=') /* for compatibility with 5.2, ... */
lua_pushfstring(L, "return %s", b + 1); /* change '=' to 'return' */
else
lua_pushlstring(L, b, l);
lua_freeline(L, b);
return 1;
}
/*
** Try to compile line on the stack as 'return <line>;'; on return, stack
** has either compiled chunk or original line (if compilation failed).
*/
static int addreturn (lua_State *L) {
const char *line = lua_tostring(L, -1); /* original line */
const char *retline = lua_pushfstring(L, "return %s;", line);
int status = luaL_loadbuffer(L, retline, strlen(retline), "=stdin");
if (status == LUA_OK) {
lua_remove(L, -2); /* remove modified line */
if (line[0] != '\0') /* non empty? */
lua_saveline(L, line); /* keep history */
}
else
lua_pop(L, 2); /* pop result from 'luaL_loadbuffer' and modified line */
return status;
}
/*
** Read multiple lines until a complete Lua statement
*/
static int multiline (lua_State *L) {
for (;;) { /* repeat until gets a complete statement */
size_t len;
const char *line = lua_tolstring(L, 1, &len); /* get what it has */
int status = luaL_loadbuffer(L, line, len, "=stdin"); /* try it */
if (!incomplete(L, status) || !pushline(L, 0)) {
lua_saveline(L, line); /* keep history */
return status; /* cannot or should not try to add continuation line */
}
lua_pushliteral(L, "\n"); /* add newline... */
lua_insert(L, -2); /* ...between the two lines */
lua_concat(L, 3); /* join them */
}
}
/*
** Read a line and try to load (compile) it first as an expression (by
** adding "return " in front of it) and second as a statement. Return
** the final status of load/call with the resulting function (if any)
** in the top of the stack.
*/
static int loadline (lua_State *L) {
int status;
lua_settop(L, 0);
if (!pushline(L, 1))
return -1; /* no input */
if ((status = addreturn(L)) != LUA_OK) /* 'return ...' did not work? */
status = multiline(L); /* try as command, maybe with continuation lines */
lua_remove(L, 1); /* remove line from the stack */
lua_assert(lua_gettop(L) == 1);
return status;
}
/*
** Prints (calling the Lua 'print' function) any values on the stack
*/
static void l_print (lua_State *L) {
int n = lua_gettop(L);
if (n > 0) { /* any result to be printed? */
luaL_checkstack(L, LUA_MINSTACK, "too many results to print");
lua_getglobal(L, "print");
lua_insert(L, 1);
if (lua_pcall(L, n, 0, 0) != LUA_OK)
l_message(progname, lua_pushfstring(L, "error calling 'print' (%s)",
lua_tostring(L, -1)));
}
}
/*
** Do the REPL: repeatedly read (load) a line, evaluate (call) it, and
** print any results.
*/
static void doREPL (lua_State *L) {
int status;
const char *oldprogname = progname;
progname = NULL; /* no 'progname' on errors in interactive mode */
lua_initreadline(L);
while ((status = loadline(L)) != -1) {
if (status == LUA_OK)
status = docall(L, 0, LUA_MULTRET);
if (status == LUA_OK) l_print(L);
else report(L, status);
}
lua_settop(L, 0); /* clear stack */
lua_writeline();
progname = oldprogname;
}
/* }================================================================== */
/*
** Main body of stand-alone interpreter (to be called in protected mode).
** Reads the options and handles them all.
*/
static int pmain (lua_State *L) {
int argc = (int)lua_tointeger(L, 1);
char **argv = (char **)lua_touserdata(L, 2);
int script;
int args = collectargs(argv, &script);
int optlim = (script > 0) ? script : argc; /* first argv not an option */
luaL_checkversion(L); /* check that interpreter has correct version */
if (args == has_error) { /* bad arg? */
print_usage(argv[script]); /* 'script' has index of bad arg. */
return 0;
}
if (args & has_v) /* option '-v'? */
print_version();
if (args & has_E) { /* option '-E'? */
lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */
lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
}
luaL_openlibs(L); /* open standard libraries */
createargtable(L, argv, argc, script); /* create table 'arg' */
lua_gc(L, LUA_GCRESTART); /* start GC... */
lua_gc(L, LUA_GCGEN, 0, 0); /* ...in generational mode */
if (!(args & has_E)) { /* no option '-E'? */
if (handle_luainit(L) != LUA_OK) /* run LUA_INIT */
return 0; /* error running LUA_INIT */
}
if (!runargs(L, argv, optlim)) /* execute arguments -e and -l */
return 0; /* something failed */
if (script > 0) { /* execute main script (if there is one) */
if (handle_script(L, argv + script) != LUA_OK)
return 0; /* interrupt in case of error */
}
if (args & has_i) /* -i option? */
doREPL(L); /* do read-eval-print loop */
else if (script < 1 && !(args & (has_e | has_v))) { /* no active option? */
if (lua_stdin_is_tty()) { /* running in interactive mode? */
print_version();
doREPL(L); /* do read-eval-print loop */
}
else dofile(L, NULL); /* executes stdin as a file */
}
lua_pushboolean(L, 1); /* signal no errors */
return 1;
}
int main (int argc, char **argv) {
int status, result;
lua_State *L = luaL_newstate(); /* create state */
if (L == NULL) {
l_message(argv[0], "cannot create state: not enough memory");
return EXIT_FAILURE;
}
lua_gc(L, LUA_GCSTOP); /* stop GC while building state */
lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */
lua_pushinteger(L, argc); /* 1st argument */
lua_pushlightuserdata(L, argv); /* 2nd argument */
status = lua_pcall(L, 2, 1, 0); /* do the call */
result = lua_toboolean(L, -1); /* get result */
report(L, status);
lua_close(L);
return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}
#endif /* LUA_MAKE_LUA */
/* /*
MIT License MIT License

View File

@ -1,8 +1,6 @@
#ifndef STATE_H #ifndef STATE_H
#define STATE_H #define STATE_H
#include <lua.h>
#include <stdbool.h> #include <stdbool.h>
typedef struct State { typedef struct State {

View File

@ -1,19 +0,0 @@
#!/bin/env sh
set +e
# check whether ninja is around (you better start running)
if [ -x "$(command -v ninja)" ]; then
generator="-G Ninja"
fi
# check whether clang is around (it's just better)
if [ -x "$(command -v clang)" ]; then
cc="-DCMAKE_C_COMPILER=clang"
fi
if [ "$1" = "web" ]; then
emcmake cmake $generator $cc -B build-web "${@:2}" && cmake --build build-web --parallel
else
cmake $generator $cc -B build "$@" && cmake --build build --parallel
fi

View File

@ -1,6 +1,6 @@
#!/bin/env sh #!/bin/env sh
# single header api generator with clang # single header api generator with clang
set +e set -e
clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format

8
bin/prep-embed.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/env sh
set -e
# packages embedded resources with objcopy, so that it is more portable
# ld.lld on windows doesn't recognize --format binary, sadly
objdump=$(objdump -i)
bdfname=$(echo "$objdump" | sed -n 2p)
objcopy -I binary -O "$bdfname" share/assets/Dernyns256.ttf "$CMAKE_CURRENT_BINARY_DIR/font.o"

33
bin/twn
View File

@ -1,21 +1,34 @@
#!/bin/env sh #!/bin/env sh
# townengine tooling interface # townengine tooling interface
set +e set -e
exe="$(basename $PWD)" exe="$(basename $PWD)"
toolpath="$(dirname -- "${BASH_SOURCE[0]}")" toolpath="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
export TWNROOT=$(realpath "$toolpath"/../) export TWNROOT=$(realpath "$toolpath"/../)
case "$1" in case "$1" in
build ) "$toolpath"/build.sh "${@:2}" build ) "$toolpath"/twnbuild "${@:2}"
;; ;;
run ) $0 build && ./$exe "${@:2}" run ) $0 build "${@:2}"
if [[ "$*" == *"--target=web"* ]]; then
if [ "$OS" = "Windows_NT" ]; then
explorer "http://0.0.0.0:8000/$exe.html" & python3 -m http.server
else
xdg-open "http://0.0.0.0:8000/$exe.html" & python3 -m http.server
fi
else
./$exe
fi
;; ;;
gdb ) unset DEBUGINFOD_URLS gdb ) unset DEBUGINFOD_URLS
$0 build && gdb --se=libgame.so -ex run --args "$(basename $PWD)" "${@:2}" $0 build --debug && gdb --se=libgame.so -ex run --args "$(basename $PWD)" --no-sanity-timer "${@:2}"
;;
init ) cp -r "$TWNROOT/apps/templates/$2" "$3"
ln -s "$TWNROOT/bin/twn" "$3/twn"
;; ;;
apitrace ) case "$2" in apitrace ) case "$2" in
@ -37,6 +50,16 @@ case "$1" in
api-gen ) "$toolpath"/gen_api_header.sh api-gen ) "$toolpath"/gen_api_header.sh
;; ;;
wiki ) if [ "$OS" = "Windows_NT" ]; then
explorer "file://""$(cygpath -w "$(command realpath $TWNROOT/docs/wiki/index.html)")"
else
xdg-open "file://$TWNROOT/docs/wiki/index.html"
fi
;;
devcompl ) (cd "$TWNROOT" && "$toolpath"/twnbuild "--build_dir=$TWNROOT/build" "${@:2}")
;;
* ) echo "Unknown command." * ) echo "Unknown command."
;; ;;
esac esac

64
bin/twnbuild Executable file
View File

@ -0,0 +1,64 @@
#!/bin/env python3
from subprocess import getoutput, run
from os import getcwd
from os.path import expandvars
from pathlib import Path
from sys import argv
from functools import reduce
import tomllib
#TODO: support for default pack override
#TODO: automatic full rebuild on git head change (such as new commits)
has_ninja = getoutput("command -v ninja") != ""
has_clang = getoutput("command -v clang") != ""
target_web = "--target=web" in argv
#TODO: infer what "native" means for current env
build_dir_arg = reduce(lambda c, n: c if c.startswith("--build_dir=") else n, argv + [""], "")
build_dir = "build/web" if target_web else build_dir_arg.split("=")[1] if build_dir_arg else "build/native"
build_dir += "" if build_dir_arg else "/release" if "--release" in argv else "/debug"
cmake = ["emcmake", "cmake"] if target_web else ["cmake"]
# cmake configuration command
command = []
# check whether clang is around (it's just better)
if has_clang and not target_web:
command += ["-DCMAKE_C_COMPILER=clang"]
# check whether ninja is around (you better start running)
if has_ninja:
command += ["-G", "Ninja"]
command += ["-B", build_dir]
# TODO: have it --fast=1 instead, where separate --debug=0 would mean stripping the debug info
if "--release" in argv:
command += ["-DCMAKE_BUILD_TYPE=Release"]
elif "--debug" in argv:
command += ["-DCMAKE_BUILD_TYPE=Debug"]
if "--unified=1" in argv:
command += ["-DTWN_FEATURE_DYNLIB_GAME=ON"]
elif "--unified=0" in argv:
command += ["-DTWN_FEATURE_DYNLIB_GAME=OFF"]
if "--sanitize=1" in argv:
command += ["-DTWN_SANITIZE=ON"]
elif "--sanitize=0" in argv:
command += ["-DTWN_SANITIZE=OFF"]
command += [f"-DTWN_OUT_DIR={getcwd()}"]
# pass arbitrary arguments over
if "--" in argv:
command += argv[argv.index("--")+1:]
# if no root cmake file is present, infer it from `twn.toml:game.interpreter`
if not Path("CMakeLists.txt").is_file():
config = tomllib.loads(Path("data/twn.toml").read_text())
command += ["-S", expandvars(config["game"]["interpreter"])]
run(cmake + command, check=True)
run(["cmake"] + ["--build", build_dir, "--parallel"], check=True)

BIN
data/assets/dirt/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/dirt/2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/grass2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/grasses/25.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
data/assets/trreez.png (Stored with Git LFS) Normal file

Binary file not shown.

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