diff --git a/MAKE.bat b/MAKE.bat index dc03717..4d8190a 100644 --- a/MAKE.bat +++ b/MAKE.bat @@ -15,9 +15,9 @@ if [ "$1" = "tidy" ]; then rm 0?-* 2> /dev/null rm fwk.o 2> /dev/null rm .art*.zip 2> /dev/null - rm demos/lua/.art*.zip 2> /dev/null + rm engine/bind/.art*.zip 2> /dev/null rm demos/html5/.art*.zip 2> /dev/null - rm demos/lua/libfwk* 2> /dev/null + rm engine/bind/libfwk* 2> /dev/null rm fwk_*.* 2> /dev/null rm 3rd_*.* 2> /dev/null rm libfwk* 2> /dev/null @@ -147,14 +147,14 @@ if [ "$(uname)" != "Darwin" ]; then chmod +x tools/xlsx2ini.linux chmod +x tools/premake5.linux chmod +x tools/ninja.linux - chmod +x demos/lua/luajit.linux + chmod +x engine/bind/luajit.linux echo build=$build, type=$dll, cc=$cc, args=$args # framework (as dynamic library) if [ "$dll" = "dll" ]; then echo libfwk.so && $cc -o libfwk.so engine/fwk.c -shared -fPIC -w -lX11 -lm -ldl -lpthread $flags $args - cp libfwk.so demos/lua/ + cp libfwk.so engine/bind/ export import="libfwk.so -Wl,-rpath,./" else # framework (static) @@ -202,14 +202,14 @@ if [ "$(uname)" = "Darwin" ]; then chmod +x tools/xlsx2ini.osx chmod +x tools/premake5.osx chmod +x tools/ninja.osx - chmod +x demos/lua/luajit.osx + chmod +x engine/bind/luajit.osx echo build=$build, type=$dll, cc=$cc, args=$args # framework (as dynamic library) if [ "$dll" = "dll" ]; then echo libfwk && cc -ObjC -dynamiclib -o libfwk.dylib engine/fwk.c -framework cocoa -framework iokit -framework audiotoolbox -w $flags $args - cp libfwk.dylib demos/lua + cp libfwk.dylib engine/bind export import=libfwk.dylib else # framework @@ -254,12 +254,12 @@ if "%1"=="help" ( echo %0 [cook] ; cook .zipfiles with tools/cook.ini cookbook echo %0 [sync] ; sync repo to latest echo %0 [tidy] ; clean up temp files - echo %0 [bindings] ; generate demos/lua bindings + echo %0 [bind] ; generate lua bindings echo %0 [checkmem] ; check untracked allocators in FWK echo %0 [split^|join] ; engine/fwk* ^>split^> engine/split/* or engine/split/* ^>join^> engine/fwk* echo %0 [amalgamation] ; combine engine/fwk* into a single-header file echo %0 [sln] ; generate a xcode/gmake/ninja/visual studio solution - echo %0 [cl^|tcc^|cc^|gcc^|clang^|clang-cl] [dbg^|dev^|rel] [static^|dll] [nofwk^|nodemos^|noeditor] [vis] [-- args] + echo %0 [cl^|tcc^|cc^|gcc^|clang^|clang-cl] [dbg^|dev^|rel] [static^|dll] [nofwk^|nodemos^|editor] [vis] [-- args] echo cl \ echo tcc ^| echo cc ^| select compiler. must be accessible in PATH @@ -273,7 +273,7 @@ if "%1"=="help" ( echo dll / link fwk as dynamic library (dll^) (default^) echo nofwk \ do not compile framework echo nodemos ^| do not compile demos - echo noeditor / do not compile editor + echo editor / do compile editor echo vis ^> visualize invokation cmdline. echo args ^> after `--` separator is found, pass all remaining arguments to compiler as-is echo. @@ -298,10 +298,10 @@ if "%1"=="cook" ( exit /b ) rem generate bindings -if "%1"=="bindings" ( +if "%1"=="bind" ( rem luajit tools\luajit tools\luajit_make_bindings.lua > fwk.lua - move /y fwk.lua demos\lua + move /y fwk.lua engine\bind exit /b ) @@ -349,7 +349,7 @@ rem generate prior files to a github release if "%1"=="github" ( rem call make.bat dll call make.bat docs - call make.bat bindings + call make.bat bind call make.bat amalgamation call make.bat split @@ -391,7 +391,7 @@ rem tidy environment if "%1"=="tidy" ( move /y ??-*.png demos > nul 2> nul move /y ??-*.c demos > nul 2> nul - del demos\lua\fwk.dll > nul 2> nul + del engine\bind\fwk.dll > nul 2> nul del .temp*.* > nul 2> nul del *.zip > nul 2> nul del *.mem > nul 2> nul @@ -428,7 +428,7 @@ set args=-Iengine set other= set fwk=yes set demos=yes -set editor=yes +set editor=no set vis=no set sln=no set rc=0 @@ -455,6 +455,7 @@ set rc=0 if "%1"=="nofwk" set "fwk=no" && goto loop if "%1"=="nodemos" set "demos=no" && goto loop if "%1"=="noeditor" set "editor=no" && goto loop + if "%1"=="editor" set "editor=yes" && goto loop if "%1"=="tcc" set "cc=%1" && goto loop if "%1"=="cl" set "cc=%1" && goto loop @@ -645,16 +646,16 @@ if not "!other!"=="" ( rem framework if "!fwk!"=="yes" ( -if "!vis!"=="yes" echo !cc! engine\fwk.c !export! !args! ^&^& if "!dll!"=="dll" copy /y fwk.dll demos\lua ^> nul -!echo! fwk && !cc! engine\fwk.c !export! !args! && if "!dll!"=="dll" copy /y fwk.dll demos\lua > nul || set rc=1 +if "!vis!"=="yes" echo !cc! engine\fwk.c !export! !args! ^&^& if "!dll!"=="dll" copy /y fwk.dll engine\bind ^> nul +!echo! fwk && !cc! engine\fwk.c !export! !args! && if "!dll!"=="dll" copy /y fwk.dll engine\bind > nul || set rc=1 ) rem editor if "!editor!"=="yes" ( set edit=-DCOOK_ON_DEMAND -DUI_LESSER_SPACING -DUI_ICONS_SMALL if "!vis!"=="yes" echo !cc! !o! editor.exe tools\editor\editor.c !edit! !import! !args! -rem !echo! editor && !cc! !o! editor.exe tools\editor\editor.c !edit! !import! !args! || set rc=1 -rem !echo! editor2 && !cc! !o! editor2.exe tools\editor\editor2.c !edit! !args! || set rc=1 +!echo! editor && !cc! !o! editor.exe tools\editor\editor.c !edit! !import! !args! || set rc=1 +!echo! editor2 && !cc! !o! editor2.exe tools\editor\editor2.c !edit! !args! || set rc=1 ) rem demos diff --git a/README.md b/README.md index 6b75a62..0e0e588 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -

F·W·K

+

F·W·K

-3D game framework in C.
+3D game engine/framework in C, with Luajit and Python bindings now.

@@ -8,12 +8,8 @@

## Goals -- [x] ~~C++~~. C. -- [x] ~~Fast~~. Naive. -- [x] ~~Modern~~. Simple. -- [x] ~~Full featured~~. Small. -- [x] ~~Rich build system~~. Single file. -- [x] ~~Royaltie fee~~. Free and unlicensed. +- [x] ~~Full featured~~, ~~Fast~~, ~~Modern C++~~. Small, Naive, Simple C. +- [x] ~~Rich build system~~, ~~Royaltie fee~~. Single file, Freely unlicensed. ## Features ᕦ(ᐛ)ᕤ - [x] Pipeline: configurable and integrated [asset pipeline](tools/cook.ini). @@ -52,25 +48,36 @@ - [x] Scene handling. - [x] Profiler, stats and leaks finder. - [x] [Editor (wip)](https://user-images.githubusercontent.com/35402248/174457347-f787a6a2-aac8-404c-a5da-f44310c3d432.mp4). -- [x] [Documentation (wip)](https://bit.ly/-f-w-k-). +- [x] [Documentation (wip)](https://bit.ly/fwk2023). ## Roadmap ᕕ(ᐛ)ᕗ (in order of arrival; ✱: partial support) - [ ] AI pass: actors, waypoints, pathfinding, behavior trees (h/fsm,goap), and navmesh generation. -- [ ] Render pass: reverse-Z, automatic LODs, impostors, decals. - - [ ] Materials: (colors✱, textures✱, matcaps✱, videos✱, shadertoys✱). Shadertoys as post-fx✱. - - [ ] Lighting: Hard/soft shadow mapping (VSM,CCSM). Baked lightmaps. Refl probes. Integrated PBR. - [ ] Network/VM pass: Entity/component/systems and worlds. + - [ ] Core pass: struct serialization. - [ ] Message pipeline and replication. - [ ] Digital signals, message buffering and event polling. - [ ] World streaming and level loading. - [ ] Scenegraphs and spatial partioning. BVH, PVS, occluders, frustum culling. - [ ] Server/client architecture. Hybrid P2P. - [ ] NAT traversal. Socketless API, message API and pub/sub wrappers (enet/websocket). +- [ ] Editor pass = netbased + offline rendering + virtual input. + - [ ] Basic: Gizmos✱, scene tree, property editor✱, load/save✱, undo/redo✱, copy/paste, on/off (vis,tick,ddraw,log), vcs. + - [ ] Scenenode: node singleton display, node console, node labels, node outlines✱. + - [ ] Debug: toggles on/off (billboards✱, materials, un/lit, cast shadows, wireframe, skybox✱/mie✱, fog/atmosphere, collide✱, physics). + - [ ] Level: volumes, triggers, platforms, level streaming. + - [ ] Sub-editor: timeline and data tracks, node graphs. + - [ ] Sub-editor: Procedural content, brushes, noise and CSG. + - [ ] Sub-editor: blendshapes, additive anims, head/foot/hand IKs. + - [ ] Script pass: DLL✱ (module->plugin/sys), Lua✱, Luajit✱, Teal✱ and TypeScript. +- [ ] Render pass: reverse-Z, automatic LODs, impostors, decals. + - [ ] Materials: (colors✱, textures✱, matcaps✱, videos✱, shadertoys✱). Shadertoys as post-fx✱. + - [ ] Lighting: Hard/soft shadow mapping (VSM,CCSM). Baked lightmaps. Refl probes. Integrated PBR. - [ ] Tools pass - [ ] Extend shaders + bindings. Per-platform✱, per-type✱, per-asset options. GIF, PKM. - [ ] Extend atlas (sprite/lightmaps). Fit packing (sprites). - [ ] Extend bindings and messaging: parse C headers during cooking stage. - [ ] API pass + - [ ] Extend math: quat2, bezier, catmull. - [ ] Discuss API and freeze it. - [ ] Document everything. @@ -183,17 +190,6 @@ int main() { } ``` -```C -#include "fwk.h" // Minimal HTML5 sample -void render(void *arg) { - if( !input(KEY_ESC) ) puts("hello FWK from HTML5!"); -} -int main() { - window_create(75.0, 0); // 75% size, no extra flags - window_loop(render, NULL); // game loop -} -``` - ```lua local fwk = require("fwk") -- Minimal Lua sample fwk.window_create(75.0,0) -- 75% size, no extra flags @@ -203,6 +199,11 @@ end ``` ## Quickstart +- Double-click `MAKE.bat` (Win) or `sh MAKE.bat` (Linux/OSX) to compile everything. +- `MAKE.bat sln` (Win) or `sh MAKE.bat sln` (Linux/OSX) to generate solutions/makefiles. +- `MAKE.bat help` (Win) or `sh MAKE.bat help` (Linux/OSX) for a bunch of options. +- `MAKE.bat hello.c` (Win) or `sh MAKE.bat hello.c` (Linux/OSX) to build a single executable. +- Alternatively, ```bat echo win/vc && cl hello.c echo win/clang-cl && clang-cl hello.c @@ -215,94 +216,108 @@ echo osx && cc -ObjC hello.c -framework cocoa -framework iokit -fr ``` ## Cook -- Most asset types need to be cooked before being used in your application. Some other assets like `.png` do not. -- Cooked assets will be written into .zipfiles close to your executable, and mounted before entering game loop. -- Cooked .zipfiles and your executable are the only required assets when releasing your game. -- Cook manually your assets by invoking supplied [`tools/cook` standalone binary](tools/). -- Cook automatically your assets by just playing your game: a runtime cook is already embedded into your binary. - - In order to achieve this, ensure the [`tools/` folder](tools/) is close to your executable. - - This folder contains all the related binaries to perform any asset conversion plus the [cookbook](tools/cook.ini) to do so. +- Assets need to be cooked before being consumed in your application. The [tools/](tools/) folder contains all the related binaries to perform any asset processing plus the [cookbook](tools/cook.ini) to do so. +- Your game will cook all your assets as long as the [`tools/`](tools/) folder is next to your executable. Alternatively, cook them all just by invoking supplied [`tools/cook` standalone binary](tools/). +- In both cases, assets will be cooked and packed into .zipfiles next to your executable, then mounted before entering game loop. These .zipfiles plus your executable are the only required files when releasing your game. ## Extra tips - Any ico/png file named after the executable name will be automatically used as app icon. - Similar to the ico/png case above, the cooked .zipfiles can be named after the main executable as well. -- Dropped files into game window will be imported & saved into [`import/`](art/engine/import) folder. -- Update the gamepad controller database by upgrading the [`gamecontrollerdb.txt`](art/engine/input) file. -- Depending on your IDE, you might need to browse to [`split/`](split/) sources when debugging FWK. +- Dropped files into game window will be imported & saved into [`import/`](engine/art/import/) folder. +- Update the gamepad controller database by upgrading the [`gamecontrollerdb.txt`](engine/art/input/) file. +- Depending on your IDE, you might need to browse to [`engine/split/`](engine/split/) sources when debugging FWK. - Cook assets on demand, as opposed to cook all existing assets on depot, by using `--cook-on-demand` flag. - Linux/OSX users can optionally install wine and use the Windows tools instead (by using `--cook-wine` flag). - Disable automatic cooking by using `--cook-jobs=0` flag (not recommended). -- Generate a project solution by dropping `split/fwk.h, fwk.c and fwk` files into it. +- Generate a project solution by dropping `engine/fwk.h, fwk.c and fwk` files into it. +- Auto-generated Luajit and Python bindings can be found in the [`engine/bind/`](engine/bind/) folder. + -## Credits (Artwork + demos) -- [Nanofactory](https://sketchfab.com/3d-models/kgirls01-d2f946f58a8040ae993cda70c97b302c), for kgirls01 3D model (CC BY-NC-ND 4.0). -- [RottingPixels](https://opengameart.org/content/2d-castle-platformer-tileset-16x16), for castle-tileset (CC0). -- [wwwtyro](https://github.com/wwwtyro/glsl-atmosphere), for nicest rayleigh/mie scattering shader around (CC0). +## Credits +**Artwork** +[Dean Evans, Raijin](https://youtu.be/RRvYkrrpMKo?t=147 "for the Map song (c)"), +[Nuulbee](https://sketchfab.com/3d-models/kgirls01-d2f946f58a8040ae993cda70c97b302c "for kgirls01 3D model (CC BY-NC-ND 4.0)"), +[Rotting Pixels](https://opengameart.org/content/2d-castle-platformer-tileset-16x16 "for castle-tileset (CC0)"), +[Tom Lewandowski](https://QuestStudios.com "for his MIDI recordings (c)"), +[Rye Terrell](https://github.com/wwwtyro/glsl-atmosphere "for nicest rayleigh/mie scattering shader around (CC0)"), +**Tools** +[Aaron Barany](https://github.com/akb825/Cuttlefish "for cuttlefish (APACHE2)"), +[Arseny Kapoulkine](https://github.com/zeux/pugixml/ "for pugixml (MIT)"), +[Assimp authors](https://github.com/assimp/assimp "for assimp (BSD3)"), +[Bernhard Schelling](https://github.com/schellingb/TinySoundFont "for tml.h (Zlib) and tsf.h (MIT)"), +[Christian Collins](http://www.schristiancollins.com "for GeneralUser GS soundfont (PD)"), +[FFMPEG authors](https://www.ffmpeg.org/ "for ffmpeg (LGPL21)"), +[Imagination](https://developer.imaginationtech.com/pvrtextool/ "for pvrtextoolcli (ITL)"), +[Krzysztof Gabis](https://github.com/kgabis/ape "for split.py/join.py (MIT)"), +[Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo "for iqm.cpp (PD)"), +[Martín Lucas Golini](https://github.com/SpartanJ/eepp/commit/8552941da19380d7a629c4da80a976aec5d39e5c "for emscripten-fs.html (CC0)"), +[Mattias Gustavsson](https://github.com/mattiasgustavsson/libs "for mid.h (PD)"), +[Michael Schmoock](http://github.com/willsteel/lcpp "for lcpp (MIT)"), +[Morgan McGuire](https://casual-effects.com/markdeep/ "for markdeep (BSD2)"), +[Olivier Lapicque, Konstanty Bialkowski](https://github.com/Konstanty/libmodplug "for libmodplug (PD)"), +[Polyglot Team](https://docs.google.com/spreadsheets/d/17f0dQawb-s_Fd7DHgmVvJoEGDMH_yoSd8EYigrb0zmM/edit "for polyglot gamedev (CC0)"), +[Tildearrow](https://github.com/tildearrow/furnace/ "for Furnace (GPL2)"), +[Tomas Pettersson](http://www.drpetter.se/ "for sfxr (PD)"), +[Tor Andersson](https://github.com/ccxvii/asstools "for assiqe.c (BSD)"), +**Runtime** +[Andreas Mantler](https://github.com/ands "for their math library (PD)"), +[Barerose](https://github.com/barerose "for swrap (CC0) and math library (CC0)"), +[Camilla Löwy](https://github.com/elmindreda "for glfw3 (Zlib)"), +[Dave Rand](https://tools.ietf.org/html/rfc1978 "for ppp (PD)"), +[David Herberth](https://github.com/dav1dde/ "for glad generated code (PD)"), +[David Reid](https://github.com/mackron "for miniaudio (PD)"), +[Dominic Szablewski](https://github.com/phoboslab/pl_mpeg "for pl_mpeg (MIT)"), +[Dominik Madarász](https://github.com/zaklaus "for json5 parser (PD)"), +[Eduard Suica](https://github.com/eduardsui/tlse "for tlse (PD)"), +[Evan Wallace](https://github.com/evanw "for their math library (CC0)"), +[Gargaj+cce/Peisik](https://github.com/gargaj/foxotron "for Foxotron/PBR shaders (UNLICENSE)"), +[Guilherme Lampert](https://github.com/glampert "for their math library (PD)"), +[Guillaume Vareille](http://tinyfiledialogs.sourceforge.net "for tinyfiledialogs (ZLIB)"), +[Haruhiko Okumura](https://oku.edu.mie-u.ac.jp/~okumura/compression/ "for lzss (PD)"), +[Igor Pavlov](https://www.7-zip.org/ "for LZMA (PD)"), +[Ilya Muravyov](https://github.com/encode84 "for bcm, balz, crush, ulz, lz4x (PD)"), +[Jon Olick](https://www.jonolick.com/ "for jo_mp1 and jo_mpeg (PD)"), +[Joonas Pihlajamaa](https://github.com/jokkebk/JUnzip "for JUnzip library (PD)"), +[Juliette Focault](https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsMaterialDesign.h "for the generated MD header (ZLIB)"), +[Kristoffer Grönlund](https://github.com/krig "for their math library (CC0)"), +[Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo "for IQM spec & player (PD)"), +[Lee Salzman, V.Hrytsenko, D.Madarász](https://github.com/zpl-c/enet/ "for enet (MIT)"), +[Libtomcrypt](https://github.com/libtom/libtomcrypt "for libtomcrypt (Unlicense)"), +[Lua authors](https://www.lua.org/ "for Lua language (MIT)"), +[Mattias Gustavsson](https://github.com/mattiasgustavsson/libs "for thread.h and https.h (PD)"), +[Micha Mettke](https://github.com/vurtun "for their math library (PD)"), +[Micha Mettke, Chris Willcocks, Dmitry Hrabrov](https://github.com/vurtun/nuklear "for nuklear (PD)"), +[Michael Galetzka](https://github.com/Cultrarius/Swarmz "for swarmz (UNLICENSE)"), +[Mārtiņš Možeiko](https://gist.github.com/mmozeiko/68f0a8459ef2f98bcd879158011cc275 "for A* pathfinding (PD)"), +[Omar Cornut, vaiorabbit](https://github.com/ocornut/imgui/pull/3627 "for tables of unicode ranges (MIT-0)"), +[Rabia Alhaffar](https://github.com/Rabios/ice_libs "for ice_batt.h (PD)"), +[Rich Geldreich](https://github.com/richgel999/miniz "for miniz (PD)"), +[Ross Williams](http://ross.net/compression/lzrw3a.html "for lzrw3a (PD)"), +[Samuli Raivio](https://github.com/bqqbarbhg/bq_websocket "for bq_websocket (PD)"), +[Sean Barrett](https://github.com/nothings "for stb_image, stb_image_write, stb_sprintf, stb_truetype and stb_vorbis (PD)"), +[Sebastian Steinhauer](https://github.com/kieselsteini "for sts_mixer (PD)"), +[Stan Melax, Cloud Wu](https://web.archive.org/web/20031204035320/http://www.melax.com/polychop/gdmag.pdf "for polychop C algorithm (PD)"), +[Stefan Gustavson](https://github.com/stegu/perlin-noise "for simplex noise (PD)"), +[Sterling Orsten](https://github.com/sgorsten "for their math library (UNLICENSE)"), +[Tor Andersson](https://github.com/ccxvii/minilibs "for xml.c (PD)"), +[Vassvik](https://github.com/vassvik/mv_easy_font "for mv_easy_font (Unlicense)"), +[Wolfgang Draxinger](https://github.com/datenwolf "for their math library (WTFPL2)"), -## Credits (Tools) -- [Aaron Barany](https://github.com/akb825/Cuttlefish), for cuttlefish (APACHE2). -- [Arseny Kapoulkine](https://github.com/zeux/pugixml/), for pugixml (MIT). -- [Assimp authors](https://github.com/assimp/assimp), for assimp (BSD3). -- [Bernhard Schelling](https://github.com/schellingb/TinySoundFont), for tml.h (Zlib) and tsf.h (MIT). -- [ffmpeg authors](https://www.ffmpeg.org/), for ffmpeg (LGPL21). -- [Imagination](https://developer.imaginationtech.com/pvrtextool/), for pvrtextoolcli (ITL). -- [Krzysztof Gabis](https://github.com/kgabis/ape), for split.py/join.py (MIT). -- [Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo), for iqm.cpp (PD). -- [Mattias Gustavsson](https://github.com/mattiasgustavsson/libs), for mid.h (PD). -- [Michael Schmoock](http://github.com/willsteel/lcpp), for lcpp (MIT). -- [Olivier Lapicque, Konstanty Bialkowski](https://github.com/Konstanty/libmodplug), for libmodplug (PD). -- [Polyglot Team](https://docs.google.com/spreadsheets/d/17f0dQawb-s_Fd7DHgmVvJoEGDMH_yoSd8EYigrb0zmM/edit), for polyglot gamedev (CC0). -- [Tildearrow](https://github.com/tildearrow/furnace/), for Furnace (GPL2). -- [Tomas Pettersson](http://www.drpetter.se/), for sfxr (PD). -- [Tor Andersson](https://github.com/ccxvii/asstools), for assiqe.c (BSD). - -## Credits (Runtime) -- [Barerose](https://github.com/barerose), for swrap (CC0). -- [Camilla Löwy](https://github.com/elmindreda), for glfw3 (Zlib). -- [Dave Rand](https://tools.ietf.org/html/rfc1978) for ppp (PD). -- [David Herberth](https://github.com/dav1dde/), for glad generated code (PD). -- [David Reid](https://github.com/mackron), for miniaudio (PD). -- [Dominic Szablewski](https://github.com/phoboslab/pl_mpeg), for pl_mpeg (MIT). -- [Dominik Madarász](https://github.com/zaklaus), for json5 parser (PD). -- [Eduard Suica](https://github.com/eduardsui/tlse), for tlse (PD). -- [Gargaj+cce/Peisik](https://github.com/gargaj/foxotron), for Foxotron/PBR shaders (UNLICENSE). -- [Guillaume Vareille](http://tinyfiledialogs.sourceforge.net), for tinyfiledialogs (ZLIB). -- [Haruhiko Okumura](https://oku.edu.mie-u.ac.jp/~okumura/compression/) for lzss (PD). -- [Igor Pavlov](https://www.7-zip.org/) for LZMA (PD). -- [Ilya Muravyov](https://github.com/encode84) for bcm, balz, crush, ulz, lz4x (PD). -- [Jon Olick](https://www.jonolick.com/), for jo_mp1 and jo_mpeg (PD). -- [Joonas Pihlajamaa](https://github.com/jokkebk/JUnzip), for JUnzip library (PD). -- [Juliette Focault](https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsMaterialDesign.h), for the generated MD header (ZLIB). -- [Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo), for IQM spec & player (PD). -- [Lee Salzman, V.Hrytsenko, D.Madarász](https://github.com/zpl-c/enet/), for enet (MIT). -- [Libtomcrypt](https://github.com/libtom/libtomcrypt), for libtomcrypt (Unlicense). -- [Lua authors](https://www.lua.org/), for Lua language (MIT). -- [Mārtiņš Možeiko](https://gist.github.com/mmozeiko/68f0a8459ef2f98bcd879158011cc275), for A* pathfinding (PD). -- [Mattias Gustavsson](https://github.com/mattiasgustavsson/libs), for thread.h and https.h (PD). -- [Micha Mettke, Chris Willcocks, Dmitry Hrabrov](https://github.com/vurtun/nuklear), for nuklear (PD). -- [Michael Galetzka](https://github.com/Cultrarius/Swarmz), for swarmz (UNLICENSE). -- [Omar Cornut, vaiorabbit](https://github.com/ocornut/imgui/pull/3627), for tables of unicode ranges (MIT-0). -- [Rabia Alhaffar](https://github.com/Rabios/ice_libs), for ice_batt.h (PD). -- [Rich Geldreich](https://github.com/richgel999/miniz), for miniz (PD). -- [Ross Williams](http://ross.net/compression/lzrw3a.html) for lzrw3a (PD). -- [Samuli Raivio](https://github.com/bqqbarbhg/bq_websocket), for bq_websocket (PD). -- [Sean Barrett](https://github.com/nothings), for stb_image, stb_image_write, stb_sprintf, stb_truetype and stb_vorbis (PD). -- [Sebastian Steinhauer](https://github.com/kieselsteini), for sts_mixer (PD). -- [Stan Melax, Cloud Wu](https://web.archive.org/web/20031204035320/http://www.melax.com/polychop/gdmag.pdf), for polychop C algorithm (PD). -- [Stefan Gustavson](https://github.com/stegu/perlin-noise), for simplex noise (PD). -- [Tor Andersson](https://github.com/ccxvii/minilibs), for xml.c (PD). -- [Vassvik](https://github.com/vassvik/mv_easy_font), for mv_easy_font (Unlicense). -- Special thanks to [@ands](https://github.com/ands), [@barerose](https://github.com/barerose), [@datenwolf](https://github.com/datenwolf), [@evanw](https://github.com/evanw), [@glampert](https://github.com/glampert), [@krig](https://github.com/krig), [@sgorsten](https://github.com/sgorsten) and [@vurtun](https://github.com/vurtun) for their math libraries (PD,CC0,WTFPL2,CC0,PD,CC0,Unlicense,PD). + ## Unlicense This software is released into the [public domain](https://unlicense.org/). Also dual-licensed as [0-BSD](https://opensource.org/licenses/0BSD) or [MIT (No Attribution)](https://github.com/aws/mit-0) for those countries where public domain is a concern (sigh). Any contribution to this repository is implicitly subjected to the same release conditions aforementioned. ## Links -

-Issues -Discord
+Still looking for alternatives? [amulet](https://github.com/ianmaclarty/amulet), [aroma](https://github.com/leafo/aroma/), [astera](https://github.com/tek256/astera), [blendelf](https://github.com/jesterKing/BlendELF), [bullordengine](https://github.com/MarilynDafa/Bulllord-Engine), [candle](https://github.com/EvilPudding/candle), [cave](https://github.com/kieselsteini/cave), [chickpea](https://github.com/ivansafrin/chickpea), [corange](https://github.com/orangeduck/Corange), [cute](https://github.com/RandyGaul/cute_framework), [dos-like](https://github.com/mattiasgustavsson/dos-like), [ejoy2d](https://github.com/ejoy/ejoy2d), [exengine](https://github.com/exezin/exengine), [gunslinger](https://github.com/MrFrenik/gunslinger), [hate](https://github.com/excessive/hate), [island](https://github.com/island-org/island), [juno](https://github.com/rxi/juno), [l](https://github.com/Lyatus/L), [lgf](https://github.com/Planimeter/lgf), [limbus](https://github.com/redien/limbus), [love](https://github.com/love2d/love/), [lovr](https://github.com/bjornbytes/lovr), [mini3d](https://github.com/mini3d/mini3d), [mintaro](https://github.com/mackron/mintaro), [mio](https://github.com/ccxvii/mio), [olive.c](https://github.com/tsoding/olive.c), [opensource](https://github.com/w23/OpenSource), [ouzel](https://github.com/elnormous/ouzel/), [pez](https://github.com/prideout/pez), [pixie](https://github.com/mattiasgustavsson/pixie), [punity](https://github.com/martincohen/Punity), [r96](https://github.com/badlogic/r96), [ricotech](https://github.com/dbechrd/RicoTech), [rizz](https://github.com/septag/rizz), [tigr](https://github.com/erkkah/tigr), [yourgamelib](https://github.com/duddel/yourgamelib) -Still looking for alternatives? -[amulet](https://github.com/ianmaclarty/amulet), [aroma](https://github.com/leafo/aroma/), [astera](https://github.com/tek256/astera), [blendelf](https://github.com/jesterKing/BlendELF), [bullordengine](https://github.com/MarilynDafa/Bulllord-Engine), [candle](https://github.com/EvilPudding/candle), [cave](https://github.com/kieselsteini/cave), [chickpea](https://github.com/ivansafrin/chickpea), [corange](https://github.com/orangeduck/Corange), [cute](https://github.com/RandyGaul/cute_framework), [dos-like](https://github.com/mattiasgustavsson/dos-like), [ejoy2d](https://github.com/ejoy/ejoy2d), [exengine](https://github.com/exezin/exengine), [gunslinger](https://github.com/MrFrenik/gunslinger), [hate](https://github.com/excessive/hate), [island](https://github.com/island-org/island), [juno](https://github.com/rxi/juno), [l](https://github.com/Lyatus/L), [lgf](https://github.com/Planimeter/lgf), [limbus](https://github.com/redien/limbus), [love](https://github.com/love2d/love/), [lovr](https://github.com/bjornbytes/lovr), [mini3d](https://github.com/mini3d/mini3d), [mintaro](https://github.com/mackron/mintaro), [mio](https://github.com/ccxvii/mio), [olive.c](https://github.com/tsoding/olive.c), [opensource](https://github.com/w23/OpenSource), [ouzel](https://github.com/elnormous/ouzel/), [pez](https://github.com/prideout/pez), [pixie](https://github.com/mattiasgustavsson/pixie), [punity](https://github.com/martincohen/Punity), [r96](https://github.com/badlogic/r96), [ricotech](https://github.com/dbechrd/RicoTech), [rizz](https://github.com/septag/rizz), [tigr](https://github.com/erkkah/tigr), [yourgamelib](https://github.com/duddel/yourgamelib) -

+Issues Discord diff --git a/demos/art/audio/larry.mid b/demos/art/audio/larry.mid new file mode 100644 index 0000000..3b886aa Binary files /dev/null and b/demos/art/audio/larry.mid differ diff --git a/demos/art/audio/larry.txt b/demos/art/audio/larry.txt new file mode 100644 index 0000000..63987f6 --- /dev/null +++ b/demos/art/audio/larry.txt @@ -0,0 +1,38 @@ + SIERRA ON-LINE, INC. + 3-D Animated Adventure Game Soundtrack Series + =============================================== + LEISURE SUIT LARRY III: PASSIONATE PATTI- + IN PURSUIT OF THE PULSATING PECTORALS + + + "TAWNI AT THE BEACH" + Mike Dana + =============================================== + Copyright (c)1989 Sierra On-Line, Inc. + =============================================== + + GENERAL MIDI VERSION + +System Requirements: + +- MIDI Playback Software capable of reading Type 1 Standard + MIDI File format +- General MIDI sound device (Wave Table or better recommended) + + +This Standard MIDI File was recorded directly from Sierra's "Leisure Suit +Larry 3" adventure game. It has been converted from the MT-32 version for +playback on General MIDI sound cards. A Wave Table or better sound card is +highly recommended for optimal playback. + +Recorded/converted for General MIDI by Tom Lewandowski. +Address questions or comments to: + + QUEST STUDIOS + Tom Lewandowski + tom@queststudios.com + + www.QuestStudios.com + + + diff --git a/demos/art/audio/waterworld-map.fur b/demos/art/audio/waterworld-map.fur new file mode 100644 index 0000000..2bf8a7a Binary files /dev/null and b/demos/art/audio/waterworld-map.fur differ diff --git a/tools/plugins/.gitkeep b/engine/bind/; type `make bind` to generate bindings similarity index 100% rename from tools/plugins/.gitkeep rename to engine/bind/; type `make bind` to generate bindings diff --git a/engine/bind/; type `make dll` to generate dll b/engine/bind/; type `make dll` to generate dll new file mode 100644 index 0000000..e69de29 diff --git a/engine/bind/MAKE.bat b/engine/bind/MAKE.bat new file mode 100644 index 0000000..68d12e1 --- /dev/null +++ b/engine/bind/MAKE.bat @@ -0,0 +1,17 @@ +#!/bin/bash 2>nul || goto :windows + +sh ../../MAKE.bat dll +sh ../../MAKE.bat bind + +./luajit.osx hello.lua +./luajit.linux hello.lua +python hello.py + +exit +:windows + +call ..\..\make.bat dll +call ..\..\make.bat bind + +luajit hello.lua +python hello.py diff --git a/demos/lua/fwk.lua b/engine/bind/fwk.lua similarity index 100% rename from demos/lua/fwk.lua rename to engine/bind/fwk.lua diff --git a/engine/bind/fwk.py b/engine/bind/fwk.py new file mode 100644 index 0000000..2f0c882 --- /dev/null +++ b/engine/bind/fwk.py @@ -0,0 +1,26 @@ +import os +import sys +import ctypes +import cffi + +ffi = cffi.FFI() +with open('./fwk.lua') as f: + lines = [line for line in f if not line.startswith('#')] + lines = [line for line in lines if not 'va_list' in line] + lines = [line for line in lines if not 'inline ' in line] + lines = [line for line in lines if not line.startswith('typedef union ') ] + lines = [line for line in lines if not '//lcpp INF' in line ] + data = ''.join(lines) + data = data[data.find('[[')+2:data.find(']]')] + data = ''' + typedef struct vec2i { float x,y; } vec2i; + typedef struct vec3i { float x,y,z; } vec3i; + typedef struct vec2 { float x,y; } vec2; + typedef struct vec3 { float x,y,z; } vec3; + typedef struct vec4 { float x,y,z,w; } vec4; + typedef struct quat { float x,y,z,w; } quat; + typedef union frustum frustum; + typedef union json_t json_t; + ''' + data + ffi.cdef(data) +fwk = ffi.dlopen('./fwk.dll') diff --git a/engine/bind/hello.lua b/engine/bind/hello.lua new file mode 100644 index 0000000..522ec4c --- /dev/null +++ b/engine/bind/hello.lua @@ -0,0 +1,56 @@ +-- this will run on vanilla luajit.exe, provided that fwk.dll and this file are all present in same folder + +local fwk=require('fwk') + +-- specify location of cookbook +fwk.cook_config("../../tools/cook.ini"); + +-- create 75% sized + MSAAx2 anti-aliased window +fwk.window_create(75.0, fwk.WINDOW_MSAA2) + +-- set window title +fwk.window_title("hello luajit") + +-- config girl +local girl = fwk.model('kgirl/kgirls01.fbx', 0) +local girl_frame = 0 +local girl_pivot = fwk.mat44() +fwk.rotationq44(girl_pivot, fwk.eulerq(fwk.vec3(0,0,0))) +fwk.scale44(girl_pivot, 2,2,2) + +-- config & play music +local music = fwk.audio_stream('larry.mid') -- 'wrath_of_the_djinn.xm' +fwk.audio_play(music, 0); + +-- config camera +local cam = fwk.camera() + +-- main loop +while fwk.window_swap() == 1 do + -- fps camera + local grabbed = fwk.input(fwk.MOUSE_L) == 1 or fwk.input(fwk.MOUSE_R) == 1 + fwk.window_cursor( fwk.ui_active() == 1 or fwk.ui_hover() == 1 and 1 or (not grabbed) ) + if( fwk.window_has_cursor() ~= 1 ) then + local wasdec3 = fwk.vec3(fwk.input(fwk.KEY_D)-fwk.input(fwk.KEY_A),fwk.input(fwk.KEY_E)-(fwk.input(fwk.KEY_C)),fwk.input(fwk.KEY_W)-fwk.input(fwk.KEY_S)) + local look2 = fwk.scale2(fwk.vec2(fwk.input_diff(fwk.MOUSE_X), -fwk.input_diff(fwk.MOUSE_Y)), 0.2) + local move3 = fwk.scale3(wasdec3, cam.speed) + fwk.camera_move(cam, wasdec3.x,wasdec3.y,wasdec3.z) + fwk.camera_fps(cam, look2.x,look2.y) + end + + -- draw grid/axis + fwk.ddraw_grid(0) + fwk.ddraw_flush() + + -- animate girl + local delta = fwk.window_delta() * 30 -- 30fps anim + girl_frame = fwk.model_animate(girl, girl_frame + delta) + + -- draw girl + fwk.model_render(girl, cam.proj, cam.view, girl_pivot, 0) + + -- showcase ui + if fwk.ui_panel("luajit", 0) == 1 then + fwk.ui_panel_end() + end +end diff --git a/engine/bind/hello.py b/engine/bind/hello.py new file mode 100644 index 0000000..54e5421 --- /dev/null +++ b/engine/bind/hello.py @@ -0,0 +1,10 @@ +import os +from fwk import fwk + +fwk.window_create(75.0, fwk.WINDOW_MSAA2) +fwk.window_title(b'hello Python') +cam = fwk.camera() +while fwk.window_swap(): + fwk.ddraw_grid(0) + +os._exit(0) \ No newline at end of file diff --git a/engine/bind/libluajit.dylib b/engine/bind/libluajit.dylib new file mode 100644 index 0000000..4d3b945 Binary files /dev/null and b/engine/bind/libluajit.dylib differ diff --git a/engine/bind/libluajit.so b/engine/bind/libluajit.so new file mode 100644 index 0000000..0ee9928 Binary files /dev/null and b/engine/bind/libluajit.so differ diff --git a/engine/bind/lua51.dll b/engine/bind/lua51.dll new file mode 100644 index 0000000..eef6c27 Binary files /dev/null and b/engine/bind/lua51.dll differ diff --git a/engine/bind/luajit.exe b/engine/bind/luajit.exe new file mode 100644 index 0000000..e7c6f97 Binary files /dev/null and b/engine/bind/luajit.exe differ diff --git a/engine/bind/luajit.linux b/engine/bind/luajit.linux new file mode 100644 index 0000000..741a312 Binary files /dev/null and b/engine/bind/luajit.linux differ diff --git a/engine/bind/luajit.osx b/engine/bind/luajit.osx new file mode 100644 index 0000000..9e957c3 Binary files /dev/null and b/engine/bind/luajit.osx differ diff --git a/engine/fwk.html b/engine/fwk.html index 6303334..c4f7683 100644 --- a/engine/fwk.html +++ b/engine/fwk.html @@ -593,13 +593,13 @@ details > summary::-webkit-details-marker { **F·W·K** -|Version: | {{VERSION}} | +|Version: | 2023.7 | |:--------------|:------------| |Branch: | main | -|Commit: | 1 | +|Commit: | 2 | -# [F·W·K {{VERSION}}](https://github.com/r-lyeh/FWK) +# [F·W·K 2023.7 ](https://github.com/r-lyeh/FWK) ## a b o u t - https://github.com/r-lyeh/FWK is a 3D game framework in C, with Luajit bindings. @@ -646,12 +646,8 @@ void assert_positive( int my_int ) { // lowercase snake_case everywhere
Goals -- [x] ~~C++~~. C. -- [x] ~~Fast~~. Naive. -- [x] ~~Modern~~. Simple. -- [x] ~~Full featured~~. Small. -- [x] ~~Rich build system~~. Single file. -- [x] ~~Royaltie fee~~. Free and unlicensed. +- [x] ~~Full featured~~, ~~Fast~~, ~~Modern C++~~. Small, Naive, Simple C. +- [x] ~~Rich build system~~, ~~Royaltie fee~~. Single file, Freely unlicensed.
@@ -694,7 +690,7 @@ void assert_positive( int my_int ) { // lowercase snake_case everywhere - [x] Scene handling. - [x] Profiler, stats and leaks finder. - [x] [Editor (wip)](https://user-images.githubusercontent.com/35402248/174457347-f787a6a2-aac8-404c-a5da-f44310c3d432.mp4). -- [x] [Documentation (wip)](https://bit.ly/-f-w-k-). +- [x] [Documentation (wip)](https://bit.ly/fwk2023). @@ -702,21 +698,32 @@ void assert_positive( int my_int ) { // lowercase snake_case everywhere - [ ] AI pass: actors, waypoints, pathfinding, behavior trees (h/fsm,goap), and navmesh generation. -- [ ] Render pass: reverse-Z, automatic LODs, impostors, decals. - - [ ] Materials: (colors✱, textures✱, matcaps✱, videos✱, shadertoys✱). Shadertoys as post-fx✱. - - [ ] Lighting: Hard/soft shadow mapping (VSM,CCSM). Baked lightmaps. Refl probes. Integrated PBR. - [ ] Network/VM pass: Entity/component/systems and worlds. + - [ ] Core pass: struct serialization. - [ ] Message pipeline and replication. - [ ] Digital signals, message buffering and event polling. - [ ] World streaming and level loading. - [ ] Scenegraphs and spatial partioning. BVH, PVS, occluders, frustum culling. - [ ] Server/client architecture. Hybrid P2P. - [ ] NAT traversal. Socketless API, message API and pub/sub wrappers (enet/websocket). +- [ ] Editor pass = netbased + offline rendering + virtual input. + - [ ] Basic: Gizmos✱, scene tree, property editor✱, load/save✱, undo/redo✱, copy/paste, on/off (vis,tick,ddraw,log), vcs. + - [ ] Scenenode: node singleton display, node console, node labels, node outlines✱. + - [ ] Debug: toggles on/off (billboards✱, materials, un/lit, cast shadows, wireframe, skybox✱/mie✱, fog/atmosphere, collide✱, physics). + - [ ] Level: volumes, triggers, platforms, level streaming. + - [ ] Sub-editor: timeline and data tracks, node graphs. + - [ ] Sub-editor: Procedural content, brushes, noise and CSG. + - [ ] Sub-editor: blendshapes, additive anims, head/foot/hand IKs. + - [ ] Script pass: DLL✱ (module->plugin/sys), Lua✱, Luajit✱, Teal✱ and TypeScript. +- [ ] Render pass: reverse-Z, automatic LODs, impostors, decals. + - [ ] Materials: (colors✱, textures✱, matcaps✱, videos✱, shadertoys✱). Shadertoys as post-fx✱. + - [ ] Lighting: Hard/soft shadow mapping (VSM,CCSM). Baked lightmaps. Refl probes. Integrated PBR. - [ ] Tools pass - [ ] Extend shaders + bindings. Per-platform✱, per-type✱, per-asset options. GIF, PKM. - [ ] Extend atlas (sprite/lightmaps). Fit packing (sprites). - [ ] Extend bindings and messaging: parse C headers during cooking stage. - [ ] API pass + - [ ] Extend math: quat2, bezier, catmull. - [ ] Discuss API and freeze it. - [ ] Document everything. @@ -833,17 +840,6 @@ int main() { } ``` -```C -#include "fwk.h" // Minimal HTML5 sample -void render(void *arg) { - if( !input(KEY_ESC) ) puts("hello FWK from HTML5!"); -} -int main() { - window_create(75.0, 0); // 75% size, no extra flags - window_loop(render, NULL); // game loop -} -``` - ```lua local fwk = require("fwk") -- Minimal Lua sample fwk.window_create(75.0,0) -- 75% size, no extra flags @@ -857,6 +853,11 @@ end
Quickstart +- Double-click `MAKE.bat` (Win) or `sh MAKE.bat` (Linux/OSX) to compile everything. +- `MAKE.bat sln` (Win) or `sh MAKE.bat sln` (Linux/OSX) to generate solutions/makefiles. +- `MAKE.bat help` (Win) or `sh MAKE.bat help` (Linux/OSX) for a bunch of options. +- `MAKE.bat hello.c` (Win) or `sh MAKE.bat hello.c` (Linux/OSX) to build a single executable. +- Alternatively, ```bat echo win/vc && cl hello.c echo win/clang-cl && clang-cl hello.c @@ -873,13 +874,9 @@ echo osx && cc -ObjC hello.c -framework cocoa -framework iokit -fr
Cook -- Most asset types need to be cooked before being used in your application. Some other assets like `.png` do not. -- Cooked assets will be written into .zipfiles close to your executable, and mounted before entering game loop. -- Cooked .zipfiles and your executable are the only required assets when releasing your game. -- Cook manually your assets by invoking supplied [`tools/cook` standalone binary](tools/). -- Cook automatically your assets by just playing your game: a runtime cook is already embedded into your binary. - - In order to achieve this, ensure the [`tools/` folder](tools/) is close to your executable. - - This folder contains all the related binaries to perform any asset conversion plus the [cookbook](tools/cook.ini) to do so. +- Assets need to be cooked before being consumed in your application. The [tools/](tools/) folder contains all the related binaries to perform any asset processing plus the [cookbook](tools/cook.ini) to do so. +- Your game will cook all your assets as long as the [`tools/`](tools/) folder is next to your executable. Alternatively, cook them all just by invoking supplied [`tools/cook` standalone binary](tools/). +- In both cases, assets will be cooked and packed into .zipfiles next to your executable, then mounted before entering game loop. These .zipfiles plus your executable are the only required files when releasing your game.
@@ -888,86 +885,100 @@ echo osx && cc -ObjC hello.c -framework cocoa -framework iokit -fr - Any ico/png file named after the executable name will be automatically used as app icon. - Similar to the ico/png case above, the cooked .zipfiles can be named after the main executable as well. -- Dropped files into game window will be imported & saved into [`import/`](art/engine/import) folder. -- Update the gamepad controller database by upgrading the [`gamecontrollerdb.txt`](art/engine/input) file. -- Depending on your IDE, you might need to browse to [`split/`](split/) sources when debugging FWK. +- Dropped files into game window will be imported & saved into [`import/`](engine/art/import/) folder. +- Update the gamepad controller database by upgrading the [`gamecontrollerdb.txt`](engine/art/input/) file. +- Depending on your IDE, you might need to browse to [`engine/split/`](engine/split/) sources when debugging FWK. - Cook assets on demand, as opposed to cook all existing assets on depot, by using `--cook-on-demand` flag. - Linux/OSX users can optionally install wine and use the Windows tools instead (by using `--cook-wine` flag). - Disable automatic cooking by using `--cook-jobs=0` flag (not recommended). -- Generate a project solution by dropping `split/fwk.h, fwk.c and fwk` files into it. +- Generate a project solution by dropping `engine/fwk.h, fwk.c and fwk` files into it. +- Auto-generated Luajit and Python bindings can be found in the [`engine/bind/`](engine/bind/) folder. +
-
Credits (Artwork + demos) +
Credits -- [Nanofactory](https://sketchfab.com/3d-models/kgirls01-d2f946f58a8040ae993cda70c97b302c), for kgirls01 3D model (CC BY-NC-ND 4.0). -- [RottingPixels](https://opengameart.org/content/2d-castle-platformer-tileset-16x16), for castle-tileset (CC0). -- [wwwtyro](https://github.com/wwwtyro/glsl-atmosphere), for nicest rayleigh/mie scattering shader around (CC0). +**Artwork** +[Dean Evans, Raijin](https://youtu.be/RRvYkrrpMKo?t=147 "for the Map song (c)"), +[Nuulbee](https://sketchfab.com/3d-models/kgirls01-d2f946f58a8040ae993cda70c97b302c "for kgirls01 3D model (CC BY-NC-ND 4.0)"), +[Rotting Pixels](https://opengameart.org/content/2d-castle-platformer-tileset-16x16 "for castle-tileset (CC0)"), +[Tom Lewandowski](https://QuestStudios.com "for his MIDI recordings (c)"), +[Rye Terrell](https://github.com/wwwtyro/glsl-atmosphere "for nicest rayleigh/mie scattering shader around (CC0)"), +**Tools** +[Aaron Barany](https://github.com/akb825/Cuttlefish "for cuttlefish (APACHE2)"), +[Arseny Kapoulkine](https://github.com/zeux/pugixml/ "for pugixml (MIT)"), +[Assimp authors](https://github.com/assimp/assimp "for assimp (BSD3)"), +[Bernhard Schelling](https://github.com/schellingb/TinySoundFont "for tml.h (Zlib) and tsf.h (MIT)"), +[Christian Collins](http://www.schristiancollins.com "for GeneralUser GS soundfont (PD)"), +[FFMPEG authors](https://www.ffmpeg.org/ "for ffmpeg (LGPL21)"), +[Imagination](https://developer.imaginationtech.com/pvrtextool/ "for pvrtextoolcli (ITL)"), +[Krzysztof Gabis](https://github.com/kgabis/ape "for split.py/join.py (MIT)"), +[Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo "for iqm.cpp (PD)"), +[Martín Lucas Golini](https://github.com/SpartanJ/eepp/commit/8552941da19380d7a629c4da80a976aec5d39e5c "for emscripten-fs.html (CC0)"), +[Mattias Gustavsson](https://github.com/mattiasgustavsson/libs "for mid.h (PD)"), +[Michael Schmoock](http://github.com/willsteel/lcpp "for lcpp (MIT)"), +[Morgan McGuire](https://casual-effects.com/markdeep/ "for markdeep (BSD2)"), +[Olivier Lapicque, Konstanty Bialkowski](https://github.com/Konstanty/libmodplug "for libmodplug (PD)"), +[Polyglot Team](https://docs.google.com/spreadsheets/d/17f0dQawb-s_Fd7DHgmVvJoEGDMH_yoSd8EYigrb0zmM/edit "for polyglot gamedev (CC0)"), +[Tildearrow](https://github.com/tildearrow/furnace/ "for Furnace (GPL2)"), +[Tomas Pettersson](http://www.drpetter.se/ "for sfxr (PD)"), +[Tor Andersson](https://github.com/ccxvii/asstools "for assiqe.c (BSD)"), +**Runtime** +[Andreas Mantler](https://github.com/ands "for their math library (PD)"), +[Barerose](https://github.com/barerose "for swrap (CC0) and math library (CC0)"), +[Camilla Löwy](https://github.com/elmindreda "for glfw3 (Zlib)"), +[Dave Rand](https://tools.ietf.org/html/rfc1978 "for ppp (PD)"), +[David Herberth](https://github.com/dav1dde/ "for glad generated code (PD)"), +[David Reid](https://github.com/mackron "for miniaudio (PD)"), +[Dominic Szablewski](https://github.com/phoboslab/pl_mpeg "for pl_mpeg (MIT)"), +[Dominik Madarász](https://github.com/zaklaus "for json5 parser (PD)"), +[Eduard Suica](https://github.com/eduardsui/tlse "for tlse (PD)"), +[Evan Wallace](https://github.com/evanw "for their math library (CC0)"), +[Gargaj+cce/Peisik](https://github.com/gargaj/foxotron "for Foxotron/PBR shaders (UNLICENSE)"), +[Guilherme Lampert](https://github.com/glampert "for their math library (PD)"), +[Guillaume Vareille](http://tinyfiledialogs.sourceforge.net "for tinyfiledialogs (ZLIB)"), +[Haruhiko Okumura](https://oku.edu.mie-u.ac.jp/~okumura/compression/ "for lzss (PD)"), +[Igor Pavlov](https://www.7-zip.org/ "for LZMA (PD)"), +[Ilya Muravyov](https://github.com/encode84 "for bcm, balz, crush, ulz, lz4x (PD)"), +[Jon Olick](https://www.jonolick.com/ "for jo_mp1 and jo_mpeg (PD)"), +[Joonas Pihlajamaa](https://github.com/jokkebk/JUnzip "for JUnzip library (PD)"), +[Juliette Focault](https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsMaterialDesign.h "for the generated MD header (ZLIB)"), +[Kristoffer Grönlund](https://github.com/krig "for their math library (CC0)"), +[Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo "for IQM spec & player (PD)"), +[Lee Salzman, V.Hrytsenko, D.Madarász](https://github.com/zpl-c/enet/ "for enet (MIT)"), +[Libtomcrypt](https://github.com/libtom/libtomcrypt "for libtomcrypt (Unlicense)"), +[Lua authors](https://www.lua.org/ "for Lua language (MIT)"), +[Mattias Gustavsson](https://github.com/mattiasgustavsson/libs "for thread.h and https.h (PD)"), +[Micha Mettke](https://github.com/vurtun "for their math library (PD)"), +[Micha Mettke, Chris Willcocks, Dmitry Hrabrov](https://github.com/vurtun/nuklear "for nuklear (PD)"), +[Michael Galetzka](https://github.com/Cultrarius/Swarmz "for swarmz (UNLICENSE)"), +[Mārtiņš Možeiko](https://gist.github.com/mmozeiko/68f0a8459ef2f98bcd879158011cc275 "for A* pathfinding (PD)"), +[Omar Cornut, vaiorabbit](https://github.com/ocornut/imgui/pull/3627 "for tables of unicode ranges (MIT-0)"), +[Rabia Alhaffar](https://github.com/Rabios/ice_libs "for ice_batt.h (PD)"), +[Rich Geldreich](https://github.com/richgel999/miniz "for miniz (PD)"), +[Ross Williams](http://ross.net/compression/lzrw3a.html "for lzrw3a (PD)"), +[Samuli Raivio](https://github.com/bqqbarbhg/bq_websocket "for bq_websocket (PD)"), +[Sean Barrett](https://github.com/nothings "for stb_image, stb_image_write, stb_sprintf, stb_truetype and stb_vorbis (PD)"), +[Sebastian Steinhauer](https://github.com/kieselsteini "for sts_mixer (PD)"), +[Stan Melax, Cloud Wu](https://web.archive.org/web/20031204035320/http://www.melax.com/polychop/gdmag.pdf "for polychop C algorithm (PD)"), +[Stefan Gustavson](https://github.com/stegu/perlin-noise "for simplex noise (PD)"), +[Sterling Orsten](https://github.com/sgorsten "for their math library (UNLICENSE)"), +[Tor Andersson](https://github.com/ccxvii/minilibs "for xml.c (PD)"), +[Vassvik](https://github.com/vassvik/mv_easy_font "for mv_easy_font (Unlicense)"), +[Wolfgang Draxinger](https://github.com/datenwolf "for their math library (WTFPL2)"), -
- -
Credits (Tools) - - -- [Aaron Barany](https://github.com/akb825/Cuttlefish), for cuttlefish (APACHE2). -- [Arseny Kapoulkine](https://github.com/zeux/pugixml/), for pugixml (MIT). -- [Assimp authors](https://github.com/assimp/assimp), for assimp (BSD3). -- [Bernhard Schelling](https://github.com/schellingb/TinySoundFont), for tml.h (Zlib) and tsf.h (MIT). -- [ffmpeg authors](https://www.ffmpeg.org/), for ffmpeg (LGPL21). -- [Imagination](https://developer.imaginationtech.com/pvrtextool/), for pvrtextoolcli (ITL). -- [Krzysztof Gabis](https://github.com/kgabis/ape), for split.py/join.py (MIT). -- [Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo), for iqm.cpp (PD). -- [Mattias Gustavsson](https://github.com/mattiasgustavsson/libs), for mid.h (PD). -- [Michael Schmoock](http://github.com/willsteel/lcpp), for lcpp (MIT). -- [Olivier Lapicque, Konstanty Bialkowski](https://github.com/Konstanty/libmodplug), for libmodplug (PD). -- [Polyglot Team](https://docs.google.com/spreadsheets/d/17f0dQawb-s_Fd7DHgmVvJoEGDMH_yoSd8EYigrb0zmM/edit), for polyglot gamedev (CC0). -- [Tildearrow](https://github.com/tildearrow/furnace/), for Furnace (GPL2). -- [Tomas Pettersson](http://www.drpetter.se/), for sfxr (PD). -- [Tor Andersson](https://github.com/ccxvii/asstools), for assiqe.c (BSD). - -
- -
Credits (Runtime) - - -- [Barerose](https://github.com/barerose), for swrap (CC0). -- [Camilla Löwy](https://github.com/elmindreda), for glfw3 (Zlib). -- [Dave Rand](https://tools.ietf.org/html/rfc1978) for ppp (PD). -- [David Herberth](https://github.com/dav1dde/), for glad generated code (PD). -- [David Reid](https://github.com/mackron), for miniaudio (PD). -- [Dominic Szablewski](https://github.com/phoboslab/pl_mpeg), for pl_mpeg (MIT). -- [Dominik Madarász](https://github.com/zaklaus), for json5 parser (PD). -- [Eduard Suica](https://github.com/eduardsui/tlse), for tlse (PD). -- [Gargaj+cce/Peisik](https://github.com/gargaj/foxotron), for Foxotron/PBR shaders (UNLICENSE). -- [Guillaume Vareille](http://tinyfiledialogs.sourceforge.net), for tinyfiledialogs (ZLIB). -- [Haruhiko Okumura](https://oku.edu.mie-u.ac.jp/~okumura/compression/) for lzss (PD). -- [Igor Pavlov](https://www.7-zip.org/) for LZMA (PD). -- [Ilya Muravyov](https://github.com/encode84) for bcm, balz, crush, ulz, lz4x (PD). -- [Jon Olick](https://www.jonolick.com/), for jo_mp1 and jo_mpeg (PD). -- [Joonas Pihlajamaa](https://github.com/jokkebk/JUnzip), for JUnzip library (PD). -- [Juliette Focault](https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsMaterialDesign.h), for the generated MD header (ZLIB). -- [Lee Salzman](https://github.com/lsalzman/iqm/tree/5882b8c32fa622eba3861a621bb715d693573420/demo), for IQM spec & player (PD). -- [Lee Salzman, V.Hrytsenko, D.Madarász](https://github.com/zpl-c/enet/), for enet (MIT). -- [Libtomcrypt](https://github.com/libtom/libtomcrypt), for libtomcrypt (Unlicense). -- [Lua authors](https://www.lua.org/), for Lua language (MIT). -- [Mārtiņš Možeiko](https://gist.github.com/mmozeiko/68f0a8459ef2f98bcd879158011cc275), for A* pathfinding (PD). -- [Mattias Gustavsson](https://github.com/mattiasgustavsson/libs), for thread.h and https.h (PD). -- [Micha Mettke, Chris Willcocks, Dmitry Hrabrov](https://github.com/vurtun/nuklear), for nuklear (PD). -- [Michael Galetzka](https://github.com/Cultrarius/Swarmz), for swarmz (UNLICENSE). -- [Omar Cornut, vaiorabbit](https://github.com/ocornut/imgui/pull/3627), for tables of unicode ranges (MIT-0). -- [Rabia Alhaffar](https://github.com/Rabios/ice_libs), for ice_batt.h (PD). -- [Rich Geldreich](https://github.com/richgel999/miniz), for miniz (PD). -- [Ross Williams](http://ross.net/compression/lzrw3a.html) for lzrw3a (PD). -- [Samuli Raivio](https://github.com/bqqbarbhg/bq_websocket), for bq_websocket (PD). -- [Sean Barrett](https://github.com/nothings), for stb_image, stb_image_write, stb_sprintf, stb_truetype and stb_vorbis (PD). -- [Sebastian Steinhauer](https://github.com/kieselsteini), for sts_mixer (PD). -- [Stan Melax, Cloud Wu](https://web.archive.org/web/20031204035320/http://www.melax.com/polychop/gdmag.pdf), for polychop C algorithm (PD). -- [Stefan Gustavson](https://github.com/stegu/perlin-noise), for simplex noise (PD). -- [Tor Andersson](https://github.com/ccxvii/minilibs), for xml.c (PD). -- [Vassvik](https://github.com/vassvik/mv_easy_font), for mv_easy_font (Unlicense). -- Special thanks to [@ands](https://github.com/ands), [@barerose](https://github.com/barerose), [@datenwolf](https://github.com/datenwolf), [@evanw](https://github.com/evanw), [@glampert](https://github.com/glampert), [@krig](https://github.com/krig), [@sgorsten](https://github.com/sgorsten) and [@vurtun](https://github.com/vurtun) for their math libraries (PD,CC0,WTFPL2,CC0,PD,CC0,Unlicense,PD). +
@@ -981,13 +992,9 @@ This software is released into the [public domain](https://unlicense.org/). Also
Links -

-Issues -Discord
+Still looking for alternatives? [amulet](https://github.com/ianmaclarty/amulet), [aroma](https://github.com/leafo/aroma/), [astera](https://github.com/tek256/astera), [blendelf](https://github.com/jesterKing/BlendELF), [bullordengine](https://github.com/MarilynDafa/Bulllord-Engine), [candle](https://github.com/EvilPudding/candle), [cave](https://github.com/kieselsteini/cave), [chickpea](https://github.com/ivansafrin/chickpea), [corange](https://github.com/orangeduck/Corange), [cute](https://github.com/RandyGaul/cute_framework), [dos-like](https://github.com/mattiasgustavsson/dos-like), [ejoy2d](https://github.com/ejoy/ejoy2d), [exengine](https://github.com/exezin/exengine), [gunslinger](https://github.com/MrFrenik/gunslinger), [hate](https://github.com/excessive/hate), [island](https://github.com/island-org/island), [juno](https://github.com/rxi/juno), [l](https://github.com/Lyatus/L), [lgf](https://github.com/Planimeter/lgf), [limbus](https://github.com/redien/limbus), [love](https://github.com/love2d/love/), [lovr](https://github.com/bjornbytes/lovr), [mini3d](https://github.com/mini3d/mini3d), [mintaro](https://github.com/mackron/mintaro), [mio](https://github.com/ccxvii/mio), [olive.c](https://github.com/tsoding/olive.c), [opensource](https://github.com/w23/OpenSource), [ouzel](https://github.com/elnormous/ouzel/), [pez](https://github.com/prideout/pez), [pixie](https://github.com/mattiasgustavsson/pixie), [punity](https://github.com/martincohen/Punity), [r96](https://github.com/badlogic/r96), [ricotech](https://github.com/dbechrd/RicoTech), [rizz](https://github.com/septag/rizz), [tigr](https://github.com/erkkah/tigr), [yourgamelib](https://github.com/duddel/yourgamelib) -Still looking for alternatives? -[amulet](https://github.com/ianmaclarty/amulet), [aroma](https://github.com/leafo/aroma/), [astera](https://github.com/tek256/astera), [blendelf](https://github.com/jesterKing/BlendELF), [bullordengine](https://github.com/MarilynDafa/Bulllord-Engine), [candle](https://github.com/EvilPudding/candle), [cave](https://github.com/kieselsteini/cave), [chickpea](https://github.com/ivansafrin/chickpea), [corange](https://github.com/orangeduck/Corange), [cute](https://github.com/RandyGaul/cute_framework), [dos-like](https://github.com/mattiasgustavsson/dos-like), [ejoy2d](https://github.com/ejoy/ejoy2d), [exengine](https://github.com/exezin/exengine), [gunslinger](https://github.com/MrFrenik/gunslinger), [hate](https://github.com/excessive/hate), [island](https://github.com/island-org/island), [juno](https://github.com/rxi/juno), [l](https://github.com/Lyatus/L), [lgf](https://github.com/Planimeter/lgf), [limbus](https://github.com/redien/limbus), [love](https://github.com/love2d/love/), [lovr](https://github.com/bjornbytes/lovr), [mini3d](https://github.com/mini3d/mini3d), [mintaro](https://github.com/mackron/mintaro), [mio](https://github.com/ccxvii/mio), [olive.c](https://github.com/tsoding/olive.c), [opensource](https://github.com/w23/OpenSource), [ouzel](https://github.com/elnormous/ouzel/), [pez](https://github.com/prideout/pez), [pixie](https://github.com/mattiasgustavsson/pixie), [punity](https://github.com/martincohen/Punity), [r96](https://github.com/badlogic/r96), [ricotech](https://github.com/dbechrd/RicoTech), [rizz](https://github.com/septag/rizz), [tigr](https://github.com/erkkah/tigr), [yourgamelib](https://github.com/duddel/yourgamelib) -

+Issues Discord
## config diff --git a/hello.c b/hello.c index 033a1da..03f29a2 100644 --- a/hello.c +++ b/hello.c @@ -3,8 +3,8 @@ // // # quickstart // - win/vc : cl hello.c -// - win/clang-cl : clang-cl hello.c -// - win/tcc : tcc hello.c -m64 +// - win/clang-cl : clang-cl hello.c +// - win/tcc : tools\tcc hello.c -m64 // - win/mingw : gcc hello.c -lws2_32 -lwinmm -ldbghelp -lole32 -luser32 -lgdi32 -lcomdlg32 // - win/clang : clang hello.c -lws2_32 -lwinmm -ldbghelp -lole32 -luser32 -lgdi32 -lcomdlg32 // - linux : cc hello.c -lm -ldl -lpthread -lX11 @@ -12,7 +12,7 @@ // - osx : cc -ObjC hello.c -framework cocoa -framework iokit -framework audiotoolbox #define FWK_IMPLEMENTATION // unrolls single-header implementation -#include "./engine/joint/fwk.h" // single-header file +#include "engine/joint/fwk.h" // single-header file int main() { // options @@ -115,6 +115,7 @@ int main() { bool enabled = fx_enabled(i); if( ui_bool(fx_name(i), &enabled) ) fx_enable(i, enabled); } + ui_panel_end(); } } diff --git a/tools/debugger.lua b/tools/debugger.lua new file mode 100644 index 0000000..b5c420e --- /dev/null +++ b/tools/debugger.lua @@ -0,0 +1,678 @@ +--[[ + Copyright (c) 2020 Scott Lembcke and Howling Moon Software + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + TODO: + * Print short function arguments as part of stack location. + * Properly handle being reentrant due to coroutines. +]] + +local dbg + +-- Use ANSI color codes in the prompt by default. +local COLOR_GRAY = "" +local COLOR_RED = "" +local COLOR_BLUE = "" +local COLOR_YELLOW = "" +local COLOR_RESET = "" +local GREEN_CARET = " => " + +local function pretty(obj, max_depth) + if max_depth == nil then max_depth = dbg.pretty_depth end + + -- Returns true if a table has a __tostring metamethod. + local function coerceable(tbl) + local meta = getmetatable(tbl) + return (meta and meta.__tostring) + end + + local function recurse(obj, depth) + if type(obj) == "string" then + -- Dump the string so that escape sequences are printed. + return string.format("%q", obj) + elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then + local str = "{" + + for k, v in pairs(obj) do + local pair = pretty(k, 0).." = "..recurse(v, depth + 1) + str = str..(str == "{" and pair or ", "..pair) + end + + return str.."}" + else + -- tostring() can fail if there is an error in a __tostring metamethod. + local success, value = pcall(function() return tostring(obj) end) + return (success and value or "") + end + end + + return recurse(obj, 0) +end + +-- The stack level that cmd_* functions use to access locals or info +-- The structure of the code very carefully ensures this. +local CMD_STACK_LEVEL = 6 + +-- Location of the top of the stack outside of the debugger. +-- Adjusted by some debugger entrypoints. +local stack_top = 0 + +-- The current stack frame index. +-- Changed using the up/down commands +local stack_inspect_offset = 0 + +-- LuaJIT has an off by one bug when setting local variables. +local LUA_JIT_SETLOCAL_WORKAROUND = 0 + +-- Default dbg.read function +local function dbg_read(prompt) + dbg.write(prompt) + io.flush() + return io.read() +end + +-- Default dbg.write function +local function dbg_write(str) + io.write(str) +end + +local function dbg_writeln(str, ...) + if select("#", ...) == 0 then + dbg.write((str or "").."\n") + else + dbg.write(string.format(str.."\n", ...)) + end +end + +local function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..":"..COLOR_YELLOW..line..COLOR_RESET end +local function format_stack_frame_info(info) + local filename = info.source:match("@(.*)") + local source = filename and dbg.shorten_path(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = (info.name and "'"..COLOR_BLUE..info.name..COLOR_RESET.."'" or format_loc(source, info.linedefined)) + return format_loc(source, info.currentline).." in "..namewhat.." "..name +end + +local repl + +-- Return false for stack frames without source, +-- which includes C frames, Lua bytecode, and `loadstring` functions +local function frame_has_line(info) return info.currentline >= 0 end + +local function hook_factory(repl_threshold) + return function(offset, reason) + return function(event, _) + -- Skip events that don't have line information. + if not frame_has_line(debug.getinfo(2)) then return end + + -- Tail calls are specifically ignored since they also will have tail returns to balance out. + if event == "call" then + offset = offset + 1 + elseif event == "return" and offset > repl_threshold then + offset = offset - 1 + elseif event == "line" and offset <= repl_threshold then + repl(reason) + end + end + end +end + +local hook_step = hook_factory(1) +local hook_next = hook_factory(0) +local hook_finish = hook_factory(-1) + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command, but are when running the print command. +local function local_bindings(offset, include_globals) + local level = offset + stack_inspect_offset + CMD_STACK_LEVEL + local func = debug.getinfo(level).func + local bindings = {} + + -- Retrieve the upvalues + do local i = 1; while true do + local name, value = debug.getupvalue(func, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the locals (overwriting any upvalues) + do local i = 1; while true do + local name, value = debug.getlocal(level, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do local i = 1; while true do + local name, value = debug.getlocal(level, -i) + if not name then break end + varargs[i] = value + i = i + 1 + end end + if #varargs > 0 then bindings["..."] = varargs end + + if include_globals then + -- In Lua 5.2, you have to get the environment table from the function's locals. + local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV) + return setmetatable(bindings, {__index = env or _G}) + else + return bindings + end +end + +-- Used as a __newindex metamethod to modify variables in cmd_eval(). +local function mutate_bindings(_, name, value) + local FUNC_STACK_OFFSET = 3 -- Stack depth of this function. + local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL + + -- Set a local. + do local i = 1; repeat + local var = debug.getlocal(level, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set local variable "..COLOR_BLUE..name..COLOR_RESET) + return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value) + end + i = i + 1 + until var == nil end + + -- Set an upvalue. + local func = debug.getinfo(level).func + do local i = 1; repeat + local var = debug.getupvalue(func, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set upvalue "..COLOR_BLUE..name..COLOR_RESET) + return debug.setupvalue(func, i, value) + end + i = i + 1 + until var == nil end + + -- Set a global. + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set global variable "..COLOR_BLUE..name..COLOR_RESET) + _G[name] = value +end + +-- Compile an expression with the given variable bindings. +local function compile_chunk(block, env) + local source = "debugger.lua REPL" + local chunk = nil + + if _VERSION <= "Lua 5.1" then + chunk = loadstring(block, source) + if chunk then setfenv(chunk, env) end + else + -- The Lua 5.2 way is a bit cleaner + chunk = load(block, source, "t", env) + end + + if not chunk then dbg_writeln(COLOR_RED.."Error: Could not compile block:\n"..COLOR_RESET..block) end + return chunk +end + +local SOURCE_CACHE = {} + +local function where(info, context_lines) + local source = SOURCE_CACHE[info.source] + if not source then + source = {} + local filename = info.source:match("@(.*)") + if filename then + pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) + elseif info.source then + for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end + end + SOURCE_CACHE[info.source] = source + end + + if source and source[info.currentline] then + for i = info.currentline - context_lines, info.currentline + context_lines do + local tab_or_caret = (i == info.currentline and GREEN_CARET or " ") + local line = source[i] + if line then dbg_writeln(COLOR_GRAY.."% 4d"..tab_or_caret.."%s", i, line) end + end + else + dbg_writeln(COLOR_RED.."Error: Source not available for "..COLOR_BLUE..info.short_src); + end + + return false +end + +-- Wee version differences +local unpack = unpack or table.unpack +local pack = function(...) return {n = select("#", ...), ...} end + +local function cmd_step() + stack_inspect_offset = stack_top + return true, hook_step +end + +local function cmd_next() + stack_inspect_offset = stack_top + return true, hook_next +end + +local function cmd_finish() + local offset = stack_top - stack_inspect_offset + stack_inspect_offset = stack_top + return true, offset < 0 and hook_factory(offset - 1) or hook_finish +end + +local function cmd_print(expr) + local env = local_bindings(1, true) + local chunk = compile_chunk("return "..expr, env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local results = pack(pcall(chunk, unpack(rawget(env, "...") or {}))) + + -- The first result is the pcall error. + if not results[1] then + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..results[2]) + else + local output = "" + for i = 2, results.n do + output = output..(i ~= 2 and ", " or "")..pretty(results[i]) + end + + if output == "" then output = "" end + dbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output) + end + + return false +end + +local function cmd_eval(code) + local env = local_bindings(1, true) + local mutable_env = setmetatable({}, { + __index = env, + __newindex = mutate_bindings, + }) + + local chunk = compile_chunk(code, mutable_env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local success, err = pcall(chunk, unpack(rawget(env, "...") or {})) + if not success then + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..tostring(err)) + end + + return false +end + +local function cmd_down() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset + 1 + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until not info or frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("Already at the bottom of the stack.") + end + + return false +end + +local function cmd_up() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset - 1 + if offset < stack_top then info = nil; break end + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("Already at the top of the stack.") + end + + return false +end + +local function cmd_where(context_lines) + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + return (info and where(info, tonumber(context_lines) or 5)) +end + +local function cmd_trace() + dbg_writeln("Inspecting frame %d", stack_inspect_offset - stack_top) + local i = 0; while true do + local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i) + if not info then break end + + local is_current_frame = (i + stack_top == stack_inspect_offset) + local tab_or_caret = (is_current_frame and GREEN_CARET or " ") + dbg_writeln(COLOR_GRAY.."% 4d"..COLOR_RESET..tab_or_caret.."%s", i, format_stack_frame_info(info)) + i = i + 1 + end + + return false +end + +local function cmd_locals() + local bindings = local_bindings(1, false) + + -- Get all the variable binding names and sort them + local keys = {} + for k, _ in pairs(bindings) do table.insert(keys, k) end + table.sort(keys) + + for _, k in ipairs(keys) do + local v = bindings[k] + + -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object. + if not rawequal(v, dbg) and k ~= "_ENV" and not k:match("%(.*%)") then + dbg_writeln(" "..COLOR_BLUE..k.. GREEN_CARET..pretty(v)) + end + end + + return false +end + +local function cmd_help() + dbg.write("" + .. COLOR_BLUE.." "..GREEN_CARET.."re-run last command\n" + .. COLOR_BLUE.." c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."continue execution\n" + .. COLOR_BLUE.." s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."step forward by one line (into functions)\n" + .. COLOR_BLUE.." n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."step forward by one line (skipping over functions)\n" + .. COLOR_BLUE.." f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."step forward until exiting the current function\n" + .. COLOR_BLUE.." u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."move up the stack by one frame\n" + .. COLOR_BLUE.." d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."move down the stack by one frame\n" + .. COLOR_BLUE.." w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[line count]"..GREEN_CARET.."print source code around the current line\n" + .. COLOR_BLUE.." e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[statement]"..GREEN_CARET.."execute the statement\n" + .. COLOR_BLUE.." p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[expression]"..GREEN_CARET.."execute the expression and print the result\n" + .. COLOR_BLUE.." t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."print the stack trace\n" + .. COLOR_BLUE.." l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."print the function arguments, locals and upvalues.\n" + .. COLOR_BLUE.." h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."print this message\n" + .. COLOR_BLUE.." q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."halt execution\n" + ) + return false +end + +local last_cmd = false + +local commands = { + ["^c$"] = function() return true end, + ["^s$"] = cmd_step, + ["^n$"] = cmd_next, + ["^f$"] = cmd_finish, + ["^p%s+(.*)$"] = cmd_print, + ["^e%s+(.*)$"] = cmd_eval, + ["^u$"] = cmd_up, + ["^d$"] = cmd_down, + ["^w%s*(%d*)$"] = cmd_where, + ["^t$"] = cmd_trace, + ["^l$"] = cmd_locals, + ["^h$"] = cmd_help, + ["^q$"] = function() dbg.exit(0); return true end, +} + +local function match_command(line) + for pat, func in pairs(commands) do + -- Return the matching command and capture argument. + if line:find(pat) then return func, line:match(pat) end + end +end + +-- Run a command line +-- Returns true if the REPL should exit and the hook function factory +local function run_command(line) + -- GDB/LLDB exit on ctrl-d + if line == nil then dbg.exit(1); return true end + + -- Re-execute the last command if you press return. + if line == "" then line = last_cmd or "h" end + + local command, command_arg = match_command(line) + if command then + last_cmd = line + -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable. + return unpack({command(command_arg)}) + elseif dbg.auto_eval then + return unpack({cmd_eval(line)}) + else + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." command '%s' not recognized.\nType 'h' and press return for a command list.", line) + return false + end +end + +repl = function(reason) + -- Skip frames without source info. + while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do + stack_inspect_offset = stack_inspect_offset + 1 + end + + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3) + reason = reason and (COLOR_YELLOW.."break via "..COLOR_RED..reason..GREEN_CARET) or "" + dbg_writeln(reason..format_stack_frame_info(info)) + + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + + repeat + local success, done, hook = pcall(run_command, dbg.read(COLOR_RED.."debugger.lua> "..COLOR_RESET)) + if success then + debug.sethook(hook and hook(0), "crl") + else + local message = COLOR_RED.."INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:"..COLOR_RESET.." "..done + dbg_writeln(message) + error(message) + end + until done +end + +-- Make the debugger object callable like a function. +dbg = setmetatable({}, { + __call = function(_, condition, top_offset, source) + if condition then return end + + top_offset = (top_offset or 0) + stack_inspect_offset = top_offset + stack_top = top_offset + + debug.sethook(hook_next(1, source or "dbg()"), "crl") + return + end, +}) + +-- Expose the debugger's IO functions. +dbg.read = dbg_read +dbg.write = dbg_write +dbg.shorten_path = function (path) return path end +dbg.exit = function(err) os.exit(err) end + +dbg.writeln = dbg_writeln + +dbg.pretty_depth = 3 +dbg.pretty = pretty +dbg.pp = function(value, depth) dbg_writeln(pretty(value, depth)) end + +dbg.auto_where = false +dbg.auto_eval = false + +local lua_error, lua_assert = error, assert + +-- Works like error(), but invokes the debugger. +function dbg.error(err, level) + level = level or 1 + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(err)) + dbg(false, level, "dbg.error()") + + lua_error(err, level) +end + +-- Works like assert(), but invokes the debugger on a failure. +function dbg.assert(condition, message) + if not condition then + dbg_writeln(COLOR_RED.."ERROR:"..COLOR_RESET..message) + dbg(false, 1, "dbg.assert()") + end + + return lua_assert(condition, message) +end + +-- Works like pcall(), but invokes the debugger on an error. +function dbg.call(f, ...) + return xpcall(f, function(err) + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(err)) + dbg(false, 1, "dbg.call()") + + return err + end, ...) +end + +-- Error message handler that can be used with lua_pcall(). +function dbg.msgh(...) + if debug.getinfo(2) then + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(...)) + dbg(false, 1, "dbg.msgh()") + else + dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Error did not occur in Lua code. Execution will continue after dbg_pcall().") + end + + return ... +end + +-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them. +local stdin_isatty = true +local stdout_isatty = true + +-- Conditionally enable the LuaJIT FFI. +local ffi = (jit and require("ffi")) +if ffi then + ffi.cdef[[ + int isatty(int); // Unix + int _isatty(int); // Windows + void free(void *ptr); + + char *readline(const char *); + int add_history(const char *); + ]] + + local function get_func_or_nil(sym) + local success, func = pcall(function() return ffi.C[sym] end) + return success and func or nil + end + + local isatty = get_func_or_nil("isatty") or get_func_or_nil("_isatty") + stdin_isatty = isatty(0) + stdout_isatty = isatty(1) +end + +-- Conditionally enable color support. +local color_maybe_supported = (stdout_isatty and os.getenv("TERM") and os.getenv("TERM") ~= "dumb") +if color_maybe_supported and not os.getenv("DBG_NOCOLOR") then + COLOR_GRAY = string.char(27) .. "[90m" + COLOR_RED = string.char(27) .. "[91m" + COLOR_BLUE = string.char(27) .. "[94m" + COLOR_YELLOW = string.char(27) .. "[33m" + COLOR_RESET = string.char(27) .. "[0m" + GREEN_CARET = string.char(27) .. "[92m => "..COLOR_RESET +end + +if stdin_isatty and not os.getenv("DBG_NOREADLINE") then + pcall(function() + local linenoise = require 'linenoise' + + -- Load command history from ~/.lua_history + local hist_path = os.getenv('HOME') .. '/.lua_history' + linenoise.historyload(hist_path) + linenoise.historysetmaxlen(50) + + local function autocomplete(env, input, matches) + for name, _ in pairs(env) do + if name:match('^' .. input .. '.*') then + linenoise.addcompletion(matches, name) + end + end + end + + -- Auto-completion for locals and globals + linenoise.setcompletion(function(matches, input) + -- First, check the locals and upvalues. + local env = local_bindings(1, true) + autocomplete(env, input, matches) + + -- Then, check the implicit environment. + env = getmetatable(env).__index + autocomplete(env, input, matches) + end) + + dbg.read = function(prompt) + local str = linenoise.linenoise(prompt) + if str and not str:match "^%s*$" then + linenoise.historyadd(str) + linenoise.historysave(hist_path) + end + return str + end + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Linenoise support enabled.") + end) + + -- Conditionally enable LuaJIT readline support. + pcall(function() + if dbg.read == nil and ffi then + local readline = ffi.load("readline") + dbg.read = function(prompt) + local cstr = readline.readline(prompt) + if cstr ~= nil then + local str = ffi.string(cstr) + if string.match(str, "[^%s]+") then + readline.add_history(cstr) + end + + ffi.C.free(cstr) + return str + else + return nil + end + end + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Readline support enabled.") + end + end) +end + +-- Detect Lua version. +if jit then -- LuaJIT + LUA_JIT_SETLOCAL_WORKAROUND = -1 + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for "..jit.version) +elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for ".._VERSION) +else + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Not tested against ".._VERSION) + dbg_writeln("Please send me feedback!") +end + +return dbg diff --git a/tools/editor/3rd_eval.h b/tools/editor/3rd_eval.h new file mode 100644 index 0000000..ead93ac --- /dev/null +++ b/tools/editor/3rd_eval.h @@ -0,0 +1,501 @@ +/* A mathematical expression evaluator. + * It uses a recursive descent parser internally. + * Author: Werner Stoop + * This is free and unencumbered software released into the public domain. + * http://unlicense.org/ + */ + +#include +#include +#include /* remember to compile with -lm */ +#include +#include +#include + +/* Special tokens used by the lexer function lex() + * they've been chosen as non-printable characters + * so that printable characters can be used for other + * purposes + */ +#define TOK_END 0 /* end of text */ +#define TOK_INI 1 /* Initial state */ +#define TOK_ID 2 /* identifier */ +#define TOK_NUM 3 /* number */ + +/* Types of errors */ + // 0 /* "no error" */ +#define ERR_MEMORY 1 /* "out of memory" */ +#define ERR_LEXER 2 /* "unknown token" */ +#define ERR_LONGID 3 /* "identifier too long" */ +#define ERR_VALUE 4 /* "value expected" */ +#define ERR_BRACKET 5 /* "missing ')'" */ +#define ERR_FUNC 6 /* "unknown function" */ +#define ERR_ARGS 7 /* "wrong number of arguments" */ +#define ERR_CONST 8 /* "unknown constant" */ + +/* Other definitions */ +#define MAX_ID_LEN 11 /* Max length of an identifier */ +#define OPERATORS "+-*/%(),^" /* Valid operators */ + +#define EVAL_PI 3.141592654 +#define EVAL_E 2.718281828 +#define EVAL_DEG (EVAL_PI/180) + +/* Internal structure for the parser/evaluator */ +struct eval { + + jmp_buf j; /* For error handling */ + + const char *p; /* Position in the text being parsed */ + + double *st; /* Stack */ + int st_size; /* Stack size */ + int sp; /* Stack pointer */ + + /* The current and next tokens identified by the lexer */ + struct { + int type; /* Type of the token */ + double n_val; /* Numeric value of the previous lexed token */ + char s_val[MAX_ID_LEN]; /* String (identifier) value of the previous lexed token */ + } token[2]; + + int cur_tok; /* Current token, either 0 or 1 (see the comments of lex()) */ +}; + +/* Prototypes */ +static double pop(struct eval *ev); +static void push(struct eval *ev, double d); +static int lex(struct eval *ev); + +/* Prototypes for the recursive descent parser */ +static void expr(struct eval *ev); +static void add_expr(struct eval *ev); +static void mul_expr(struct eval *ev); +static void pow_expr(struct eval *ev); +static void uni_expr(struct eval *ev); +static void bra_expr(struct eval *ev); +static void id_expr(struct eval *ev); +static void num_expr(struct eval *ev); + +/* + * Evaluates a mathemeatical expression + */ +double eval(const char *exp/*, int *ep*/) { +int _ep, *ep = &_ep; + struct eval ev; + double ans = 0.0; + + assert(ep != NULL); + + /* Allocate a stack */ + ev.st_size = 10; + ev.st = CALLOC(ev.st_size, sizeof *ev.st); + if(!ev.st) + { + *ep = ERR_MEMORY; + return NAN; //0.0; + } + ev.sp = 0; + + /* Manage errors */ + *ep = setjmp(ev.j); + if(*ep != 0) + { + FREE(ev.st); + return NAN; //0.0; + } + + /* Initialize the lexer */ + ev.token[0].type = TOK_INI; + ev.token[0].s_val[0] = '\0'; + ev.token[1].type = TOK_INI; + ev.token[1].s_val[0] = '\0'; + ev.cur_tok = 0; + + /* Initialize the parser */ + ev.p = exp; + + /* lex once to initialize the lexer */ + if(lex(&ev) != TOK_END) + { + expr(&ev); + ans = pop(&ev); + } + + FREE(ev.st); + return ans; +} + +/* + * Pushes a value onto the stack, increases the stack size if necessary + */ +static void push(struct eval *ev, double d) { + if(ev->sp == ev->st_size) { + /* Resize the stack by 1.5 */ + double *old = ev->st; + int new_size = ev->st_size + (ev->st_size >> 1); + ev->st = REALLOC(ev->st, new_size); + if(!ev->st) { + ev->st = old; + longjmp(ev->j, ERR_MEMORY); + } + + ev->st_size = new_size; + } + + ev->st[ev->sp++] = d; +} + +// Pops a value from the top of the stack +static double pop(struct eval *ev) { + assert(ev->sp > 0); + return ev->st[--ev->sp]; +} + +// stricmp() is common, but not standard, so I provide my own +static int istrcmp(const char *p, const char *q) { + for(; tolower(p[0]) == tolower(q[0]) && p[0]; p++, q++); + return tolower(p[0]) - tolower(q[0]); +} + +/* + * Lexical analyzer function + * + * In order to implement LL(1), struct eval has an array of two token structures, + * and its cur_tok member is used to point to the _current_ token, while the other + * element contains the _next_ token. This implements a 2 element ring buffer where + * the lexer always writes to the _next_ token so that the recursive descent parser can + * _peek_ at the next token. + */ + +static int lex(struct eval *ev) { + int next_tok; + +start: + /* Cycle the tokens */ + next_tok = ev->cur_tok; + ev->cur_tok = ev->cur_tok?0:1; + + while(isspace(ev->p[0])) ev->p++; + + if(!ev->p[0]) { + /* End of the expression */ + ev->token[next_tok].type = TOK_END; + goto end; + } + else if(isdigit(ev->p[0]) || ev->p[0] == '.') { + /* Number */ + char *endp; + ev->token[next_tok].type = TOK_NUM; + ev->token[next_tok].n_val = strtod(ev->p, &endp); + ev->p = endp; + goto end; + } + else if(isalpha(ev->p[0])) { + /* Identifier */ + int i; + for(i = 0; isalnum(ev->p[0]) && i < MAX_ID_LEN - 1; i++, ev->p++) + ev->token[next_tok].s_val[i] = ev->p[0]; + + if(isalpha(ev->p[0])) longjmp(ev->j, ERR_LONGID); + + ev->token[next_tok].s_val[i] = '\0'; + ev->token[next_tok].type = TOK_ID; + goto end; + } + else if(strchr(OPERATORS, ev->p[0])) { + /* Operator */ + ev->token[next_tok].type = ev->p[0]; + ev->p++; + goto end; + } + else /* Unknown token */ + longjmp(ev->j, ERR_LEXER); + +end: + + /* If this was the first call, cycle the tokens again */ + if(ev->token[ev->cur_tok].type == TOK_INI) + goto start; + + return ev->token[ev->cur_tok].type; +} + +#define EVAL_TYPE(e) (e->token[e->cur_tok].type) +#define EVAL_ERROR(c) longjmp(ev->j, (c)) + +// num_expr ::= NUMBER +static void num_expr(struct eval *ev) { + if(EVAL_TYPE(ev) != TOK_NUM) + EVAL_ERROR(ERR_VALUE); + push(ev, ev->token[ev->cur_tok].n_val); + lex(ev); +} + +// expr ::= add_expr +static void expr(struct eval *ev) { + add_expr(ev); +} + +// add_expr ::= mul_expr [('+'|'-') mul_expr] +static void add_expr(struct eval *ev) { + int t; + mul_expr(ev); + while((t =EVAL_TYPE(ev)) == '+' || t == '-') { + double a,b; + lex(ev); + mul_expr(ev); + b = pop(ev); + a = pop(ev); + + if(t == '+') + push(ev, a + b); + else + push(ev, a - b); + } +} + +// mul_expr ::= pow_expr [('*'|'/'|'%') pow_expr] +static void mul_expr(struct eval *ev) { + int t; + pow_expr(ev); + while((t = EVAL_TYPE(ev)) == '*' || t == '/' || t == '%') { + double a,b; + lex(ev); + pow_expr(ev); + b = pop(ev); + a = pop(ev); + + if(t == '*') + push(ev, a * b); + else if(t == '/') + push(ev, a / b); + else + push(ev, fmod(a, b)); + } +} + +// pow_expr ::= uni_expr ['^' pow_expr] +static void pow_expr(struct eval *ev) { + /* Note that exponentiation is right associative: + 2^3^4 is 2^(3^4), not (2^3)^4 */ + uni_expr(ev); + if(EVAL_TYPE(ev) == '^') { + double a,b; + lex(ev); + pow_expr(ev); + b = pop(ev); + a = pop(ev); + push(ev, pow(a,b)); + } +} + +// uni_expr ::= ['+'|'-'] bra_expr +static void uni_expr(struct eval *ev) { + int t = '+'; + if(EVAL_TYPE(ev) == '-' || EVAL_TYPE(ev) == '+') { + t = EVAL_TYPE(ev); + lex(ev); + } + + bra_expr(ev); + + if(t == '-') { + double a = pop(ev); + push(ev, -a); + } +} + +// bra_expr ::= '(' add_expr ')' | id_expr +static void bra_expr(struct eval *ev) { + if(EVAL_TYPE(ev) == '(') { + lex(ev); + add_expr(ev); + if(EVAL_TYPE(ev) != ')') + EVAL_ERROR(ERR_BRACKET); + lex(ev); + } + else + id_expr(ev); +} + +// id_expr ::= ID '(' add_expr [',' add_expr]* ')' | ID | num_expr +static void id_expr(struct eval *ev) { + int nargs = 0; + char id[MAX_ID_LEN]; + if(EVAL_TYPE(ev) != TOK_ID) { + num_expr(ev); + } else { + strcpy(id, ev->token[ev->cur_tok].s_val); + lex(ev); + if(EVAL_TYPE(ev) != '(') { + /**/ if(!istrcmp(id, "true")) push(ev, 1.0); + else if(!istrcmp(id, "false")) push(ev, 0.0); + else if(!istrcmp(id, "on")) push(ev, 1.0); + else if(!istrcmp(id, "off")) push(ev, 0.0); + // pi - 3.141592654 + else if(!istrcmp(id, "pi")) + push(ev, EVAL_PI); + // e - base of natural logarithms, 2.718281828 + else if(!istrcmp(id, "e")) + push(ev, EVAL_E); + // deg - deg2rad, allows to degree conversion `sin(90*deg) = 1` + else if(!istrcmp(id, "deg")) + push(ev, EVAL_DEG); + else + EVAL_ERROR(ERR_CONST); + } else { + lex(ev); + + while(EVAL_TYPE(ev) != ')') { + add_expr(ev); + nargs++; + if(EVAL_TYPE(ev) == ')') break; + + if(EVAL_TYPE(ev) != ',') + EVAL_ERROR(ERR_BRACKET); + lex(ev); + } + lex(ev); + + // abs(x) - absolute value of x + if(!istrcmp(id, "abs")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, fabs(pop(ev))); + } + // ceil(x) - smallest integer greater than x + else if(!istrcmp(id, "ceil")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, ceil(pop(ev))); + } + // floor(x) - largest integer smaller than x + else if(!istrcmp(id, "floor")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, floor(pop(ev))); + } + // sin(x) - sine of x, in radians + else if(!istrcmp(id, "sin")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, sin(pop(ev))); + } + // asin(x) - arcsine of x, in radians + else if(!istrcmp(id, "asin")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, asin(pop(ev))); + } + // cos(x) - cosine of x, in radians + else if(!istrcmp(id, "cos")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, cos(pop(ev))); + } + // acos(x) - arccosine of x, in radians + else if(!istrcmp(id, "acos")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, acos(pop(ev))); + } + // tan(x) - tangent of x, in radians + else if(!istrcmp(id, "tan")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, tan(pop(ev))); + } + // atan(x) - arctangent of x, in radians + else if(!istrcmp(id, "atan")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, atan(pop(ev))); + } + // atan(y,x) - arctangent of y/x, in radians. + else if(!istrcmp(id, "atan2")) { + double a, b; + if(nargs != 2) EVAL_ERROR(ERR_ARGS); + b = pop(ev); + a = pop(ev); + push(ev, atan2(a,b)); + } + // sinh(x) - hyperbolic sine of x, in radians + else if(!istrcmp(id, "sinh")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, sinh(pop(ev))); + } + // cosh(x) - hyperbolic cosine of x, in radians + else if(!istrcmp(id, "cosh")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, cosh(pop(ev))); + } + // tanh(x) - hyperbolic tangent of x, in radians + else if(!istrcmp(id, "tanh")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, tanh(pop(ev))); + } + // log(x) - natural logarithm of x + else if(!istrcmp(id, "log")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, log(pop(ev))); + } + // log10(x) - logarithm of x, base-10 + else if(!istrcmp(id, "log10")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, log10(pop(ev))); + } + // exp(x) - computes e^x + else if(!istrcmp(id, "exp")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, exp(pop(ev))); + } + // sqrt(x) - square root of x + else if(!istrcmp(id, "sqrt")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, sqrt(pop(ev))); + } + // rad(x) - converts x from degrees to radians + else if(!istrcmp(id, "rad")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, pop(ev)*EVAL_PI/180); + } + // deg(x) - converts x from radians to degrees + else if(!istrcmp(id, "deg")) { + if(nargs != 1) EVAL_ERROR(ERR_ARGS); + push(ev, pop(ev)*180/EVAL_PI); + } + // pow(x,y) - computes x^y + else if(!istrcmp(id, "pow")) { + double a, b; + if(nargs != 2) EVAL_ERROR(ERR_ARGS); + b = pop(ev); + a = pop(ev); + push(ev, pow(a,b)); + } + // hypot(x,y) - computes sqrt(x*x + y*y) + else if(!istrcmp(id, "hypot")) { + double a, b; + if(nargs != 2) EVAL_ERROR(ERR_ARGS); + b = pop(ev); + a = pop(ev); + push(ev, sqrt(a*a + b*b)); + } + else + EVAL_ERROR(ERR_FUNC); + } + } +} + +// + +#ifdef EVALDEMO +#include +int main(int argc, char *argv[]) { + int i; + double e; + + for(i = 1; i < argc; i++) { + e = eval(argv[i]); + if(e != e) + fprintf(stderr, "Error in expression %s\n", argv[i]); + else + printf("%s = %g\n", argv[i], e); + } + + assert( eval("1+1") == 2 ); + assert( eval("1+") != eval("1+") ); + assert(~puts("Ok") ); +} +#endif diff --git a/tools/editor/editor b/tools/editor/editor new file mode 100644 index 0000000..6122a69 --- /dev/null +++ b/tools/editor/editor @@ -0,0 +1,82 @@ +#ifdef ICON +// 2 states +ICON(DONE),ICON(DONE_ALL), +ICON(CHECK_BOX),ICON(CHECK_BOX_OUTLINE_BLANK), +ICON(VISIBILITY),ICON(VISIBILITY_OFF), +ICON(NOTIFICATIONS),ICON(NOTIFICATIONS_OFF), + +ICON(TOGGLE_ON),ICON(TOGGLE_OFF), +ICON(LIGHTBULB),ICON(LIGHTBULB_OUTLINE), +ICON(FULLSCREEN),ICON(FULLSCREEN_EXIT), +ICON(VIDEOCAM),ICON(VIDEOCAM_OFF), + +ICON(FIBER_MANUAL_RECORD),ICON(FIBER_SMART_RECORD), +ICON(EXPAND_MORE),ICON(EXPAND_LESS), +ICON(SPORTS_ESPORTS),ICON(VIDEOGAME_ASSET),ICON(GAMEPAD),//ICON(STADIA_CONTROLLER), +ICON(FILE_DOWNLOAD),ICON(FILE_UPLOAD), + +// 3 states +ICON(FOLDER_OPEN),ICON(CREATE_NEW_FOLDER),ICON(FOLDER_SPECIAL), +ICON(CONTENT_CUT),ICON(CONTENT_COPY),ICON(CONTENT_PASTE), +ICON(STAR),ICON(STAR_HALF),ICON(STAR_OUTLINE), +ICON(VOLUME_DOWN),ICON(VOLUME_UP),ICON(VOLUME_OFF), + +// 6 states +ICON(FAST_REWIND),ICON(SKIP_PREVIOUS),ICON(PLAY_ARROW),ICON(SKIP_NEXT),ICON(FAST_FORWARD),ICON(REPEAT), + +// 12 states +ICON(POWER),ICON(BATTERY_CHARGING_FULL),ICON(BATTERY_FULL),ICON(BATTERY_6_BAR),ICON(BATTERY_5_BAR),ICON(BATTERY_4_BAR),ICON(BATTERY_3_BAR),ICON(BATTERY_2_BAR),ICON(BATTERY_1_BAR),ICON(BATTERY_0_BAR),ICON(BATTERY_ALERT), + + +ICON(MOVIE), +ICON(CAMERA),ICON(PHOTO_CAMERA), +ICON(SPEED), +ICON(ROOM), +ICON(PUSH_PIN), +ICON(FLAG), + +ICON(G_TRANSLATE), +ICON(SETTINGS), +ICON(CLOSED_CAPTION), + +ICON(BUILD), +ICON(ROCKET_LAUNCH), + +ICON(TODAY),ICON(EVENT_NOTE), + +ICON(3D_ROTATION), +ICON(LAUNCH), +ICON(SEARCH), +ICON(TIMELAPSE), +ICON(ARROW_DOWNWARD), +ICON(CALL_MERGE), +ICON(ARCHIVE), +ICON(SHOW_CHART), +ICON(WARNING), +ICON(CREATE),ICON(ADD), + +ICON(TEXT_FORMAT), +ICON(CHECK), +ICON(SAVE), +ICON(CANCEL), +ICON(DELETE), +ICON(CLOSE), +ICON(REFRESH), +ICON(SYNC), +ICON(HIGHLIGHT_OFF), +ICON(SYSTEM_UPDATE), +ICON(UNDO),ICON(REDO), +ICON(CLASS), +ICON(TITLE), +ICON(HD), + +ICON(VIEW_QUILT), + +ICON(FINGERPRINT), +ICON(VPN_KEY), +ICON(FACE),ICON(PERSON), +ICON(CHAT_BUBBLE), + +ICON(COPYRIGHT), +#undef ICON +#endif diff --git a/tools/editor/editor.c b/tools/editor/editor.c new file mode 100644 index 0000000..05b220b --- /dev/null +++ b/tools/editor/editor.c @@ -0,0 +1,1836 @@ +// in-game editor +// - rlyeh, public domain. + +// ## Design +// ### editor (v1) +// The v1 editor is a tool that understands Assets and is able to *edit the details of such Assets*. +// This understanding is configured via reflection fields in .ini files. Can be reflected from C as well. +// The reflected properties will enable loading, saving and creating generic widget views to edit the Assets. +// Because we load and save the state of Assets, we can also undo and redo changes by simulating load/saves from/into memory. +// And we can also dump the contents into disk, and create diffs and patches from them. +// - [x] Load Assets +// - [x] Edit Assets +// - [x] Save Assets +// - [x] Undo Assets (automatic, via loading older state) +// - [x] Redo Assets (automatic, via loading newer state) +// - [x] Diff Assets (from two states) +// - [x] Mend Assets (from two states) +// +// Note that the editor is dumb and does not tick/draw your GameObjects. It does not handle the Scene/World either. +// Those are game-driven systems. Your game should provide the meanings to actually: +// - [?] Spawn Assets +// - [?] Delete Assets +// - [x] Draw Assets +// - [x] Tick Assets +// - [*] Scene Traversals (parent->children*, visibility, collisions and such) +// +// PS: Asset pipeline is external to the editor. Exotic assets could use some fancy plugins to deal with the import/export; eg, Substance 3D. +// PS: Asset versioning is also external to the editor. We could integrate a few VCS plugins within the editor: p4, git, svn, ... +// +// ### editor (v2) +// The v2 editor adds container support and modding features from previous editor. +// +// Your game could use fancy containers everywhere now. However, for simplicity purposes, Editor would be ignorant about them as well. +// Editor can only use containers that can decay to vectors and strides. Examples: +// - [x] Vectors: already supported in the Editor. +// - [?] Arrays: can decay to a fixed/immutable vector. +// - [?] Sparse/Unordered/Ordered Sets: can decay to vector of values. +// - [?] Sparse/Unordered/Ordered Maps: can decay to vector of keys + vector of values. +// - [?] Other fancy containers: can iterate on elements and send each item to editor individually; might be slow. +// +// We also allow here for others to extend or *override the behavior and look of each window and/or widget* by using .lua and .dll plugins: +// - [ ] Draw Windows --> Custom overrides to alter or enhance the renderer while editing. Via C/C++/dll/lua plugins +// - [ ] Tick Windows --> Custom overrides to alter or enhance the behavior while editing. Via C/C++/dll/lua plugins +// +// ### editor (v3) +// v3 brings in data driven control; ie, be able to parse & interpret text commands from any input stream. +// This would allow for remote control (via OSC), extending scripts, offline commands, Telnet sessions or external tools; like GDB does. +// - [ ] Data driven +// +// The v3 editor is also a bootstrapped v2 editor with tons of .luas. The C skeleton is only a window manager at this point. +// The intention here is to *leverage editing workflow purely into data-driven files*, so the engine can grow up exponentially from here. +// Data-driven on steroids. It would be totally a success if the editor could be bootstrapped to include these kind of sub-editors without much work on the C codebase: +// - [ ] Level 2D/Blockout editor +// - [ ] Level 3D/Blockout editor +// - [*] World outliner +// - [ ] Nodegraph editor (ShaderGraph, AnimGraph, AudioGraph, MaterialGraph, DialogGraph, AIGraph, QuestGraph, ...) +// - [ ] Sequencer +// - [ ] Tracker (music tracker) +// - [ ] Etc... +// +// ### editor (v4) +// Bring in remote datas into the editor. +// Go social & marketplace. Allow others to expand, share, publish, subscribe, discuss their sub-editors within a small community. +// I really like the way the way OpenFrameworks.cc does their addons, and I think we should do same: just discover and monitor github repos, and list everything on a website (fwk- prefix?). +// Wishlist for a github-based community flow: discovery, transparent installs, publish on github, star there, watch commits & releases, track issues+discussions, etc +// +// We should have a generic, extensible, script/plugin-driven, working editor at this point (hopefully) that does not require maintenance. + +// ## Roadmaps +// ### v1 roadmap (current) +// - [*] menu: open, save, save as, save all, reload +// - [x] basic gizmos (@todo: fixed screen size, snapping) +// - [ ] add/rem entities, add/rem components, add/rem/pause/resume systems +// - [ ] cut/copy/paste (ctrl-c to serialize) +// - [ ] F1, detach from game (long press will send F1 key to game) +// - [ ] TAB, isolated view of selected entity on/off. (long press will send TAB key to game) +// - [ ] standardise binary format. msgpack(<->json)? go .ini instead? .ini+blob? .kvdb? +// - [*] object processing from game: tick,draw*,spawn,delete,un/load from bvh stream, +// - [ ] cut/copy/paste <--> add/del events into game (ctrl-c to serialize) +// - [x] multiple selections/select all +// - [x] tree traversal from game (parent->child) +// - [ ] operations on trees: load/save -> as filesystem or .zipped level +// +// ### v2 roadmap (mid-term) +// - [ ] add keyboard shortcuts +// - [ ] tree traversal from game +// - [ ] bvh and collision queries +// - [ ] visibility and pvs queries +// - [ ] art/ vs prefabs/ discrimination: prefabs/ are archetypes (composed types); ie, data-classes. art/ contains data files. +// - [ ] can prefabs be done with ecs maybe? +// - [ ] example: levels are prefabs, composed of other sub-prefabs or art assets. +// - [ ] example: hitboxes+events. girl=pivot(p,r,s)+model(mesh,tex)+curframe +// - [ ] extend widgets vec3 as range;customized mesh,texture,audio,any other asset,combo of anything) +// +// ### v3 roadmap (long-term) +// ### v4 roadmap (long-term) +// - [ ] osc server for properties and editor behavior +// + +// ## Implementation ideas +// ### editor +// - [x] editor = tree of nodes. world, levels and objects are nodes, and even widgets are also nodes. +// - [ ] you can perform actions on some or all of these nodes, with or without descendants, from any top-bottom or bottom-top directions. +// - [ ] these actions include load/save, reset, undo/redo, play/render, toggle vis:on/off/alpha logic:on/off/other ddraw:on/off log:on/off, etc. +// and that's all. +// +// ### organization: world as a filesystem +// - [ ] anything can be serialized into disk. any object, any entity, any property or any widget can be serialized into disk. +// - [ ] groups of them as well. the whole world state can be serialized into disk as a filesystem snapshot: +// - [ ] - entities are folders. you can attach nodes on nodes (ie, create folders inside folders). +// - [ ] - systems are dlls/scripts. you can modify them on the fly and they should reload. +// - [ ] - components are data files. each component is a file. some components may be pure datas (ie, raw textures) but some others can be human-readable and editable. +// inside of that, every line is a JSON/INI property that you can tweak, modify or inspect. +// +// ### replication: diffing zips +// - [ ] the whole world/filesystem will be compressed into a zipfile and delivered to the network when sharding/replicating in a network scenario. +// - [ ] clients will be diffing/patching their filesystems on every received packet. there will be 3 operations to support internally that will reflect what the E/C/S world is doing behind the curtains: +// - [ ] - added files/folders [+] : when creating entities/components/systems +// - [ ] - deleted files/folders [-] : when removing entities/components/systems +// - [ ] - modifying files/folders [*] : when altering entities/components/systems +// +// ### communication: osc messages +// - [ ] any living entity in the game, or within the editor, can be inspected, debugged or tweaked from external tools. +// - [ ] in order to achieve that, an opensoundserver is listening on a binding IP and you can send UDP packets to every node in the world. +// - [ ] the UDP port number matches current year (2021, 2022, 2023...) +// +// ### augmentation: widgets escalate from bottom +// - [x] there are only a few basic supplied widgets. +// - [x] and they correlate to C types: bool, u/int 8/16/32/64, float/double, strings and enums. +// - [x] structs are covered by reflecting and editing all members separately. +// - [ ] optionally, you can extend some basic types to have better visualization widgets. +// ie, you could alias x4 float widgets together into a new shiny vec4 widget that is more compact, fancy and convenient to use. +// then you can also alias that very same vec4 into a color picker for example; or maybe convert that vec3 into a position gizmo. +// then maybe alias x2 color pickers and create a gradient widget. and so on... + +// ## old notes below +// ================== +// - [ ] editor (json level): load/save jsons, property editor for anything (remote osc server/client) +// - gizmo: proportional, orbit/arcball XY (+shift for Z/tilt) +// - scene: scenegraph, obj naming, ~~obj picking, obj bounds,~~ obj collisions, obj/scene streaming +// - placeholders google +// - vcs +// - [ ] Level objects: ~~volumes, triggers, platforms, streaming~~. +// - level: emitters: particles, lights, lightmaps, sound sources, triggers, etc +// - level: box triggers, start/end, spawn, streaming, checkpoints, +// - level: jump, shoots, platforms, collisions +// - level: 60s, 70s, 80s, 90s +// - [ ] Core: wecs+replication +// - modules: script or dll + ram load/save/diff/patch + play/stop/init/ + attach/detach +// - logic tree/ << [] |> || >> +// - - scene |> +// - - enemies +// - ecs: sys are modules, ecs: char *messaging, ecs: filesystem (e/dir,c/files,s/dll) +// - world: streaming, migration + +#include "fwk.h" + +// #include "labs.vm/ecs.c" + +#define EDITOR_VERSION "2022.7" + +#if 1 +#define EDITOR_PRINTF PRINTF +#else +#define EDITOR_PRINTF(...) do {} while(0) +#endif + +// editor controls + +//static int editor_attached = 1; +static int editor_enabled = 1; +static void* editor_selected_obj = 0; +static int editor_key = 0; +static vec2 editor_mouse = {0}; // 2d coord for ray/picking +static bool editor_gamepad = 1; +static int editor_hz = 60; +static int editor_hz_mid = 18; +static int editor_hz_low = 5; +static bool editor_power_saving = 0; +static double editor_t = 0, editor_dt = 0; +static bool editor_lit = 1; +static bool editor_ddraw = 1; + +static +void editor_init_variables() { +} + +bool editor_active() { + return ui_hover() || ui_active() || gizmo_active() ? editor_enabled : 0; +} +double editor_ss() { + return 1000 + editor_t; +} +double editor_delta() { + return editor_dt; +} + + +enum editor_keys { + key_none, + key_pause, + key_reload, + key_browser, + key_recording, + key_fullscreen, + key_screenshot, // @todo: add meta-info in exif or invisibile pixels (cam details, player details, map level, map location, level state, etc) + key_quit, + key_mute, + key_battery, + key_profiler, + key_stop, + key_outliner, + key_undo, + key_redo, + key_save_mem, + key_save_disk, + key_load_disk, + key_reset, + key_debugger, + key_gamepad, + key_lit, + key_ddraw, +}; + +// editor core + +typedef void* obj_t; +typedef array(obj_t) objs_t; + +typedef struct property { // meta: "vec3 namespace.position = {1,2,3}; // minv=(0,0,0) key1=value1 key2=value2 [...] @this is a tooltip @@this is a comment" + char *mark; // namespace + char *name; // display name + char *type; // pointed type + char *hint; // name@tooltip + char *minv; // min value + char *maxv; // max value +#if 0 // @todo: implement me + char *incv; // inc value + char *defv; // default value + char *isro; // is read-only/enabled + char *issv; // is save pending +#endif + void *value; + unsigned typebits; + unsigned flags; +} property; + + +// low-level operations +int save1(bool apply, array(char) *buffer, array(property) arrp) { + // iterate and save + unsigned total = 0; + for each_array_ptr(arrp, property, p) { + unsigned bytes = 0; + /**/ if( p->type[0] == 'f') bytes = sizeof(float); + else if( p->type[0] == 'v') bytes = sizeof(vec3); + else if( p->type[0] == 'i') bytes = sizeof(int); + else if( p->type[0] == 'b') bytes = sizeof(bool); + + if( !apply ) continue; + + if( bytes ) { + array_resize(*buffer, array_count(*buffer) + bytes); + memcpy( &(*buffer)[array_count(*buffer) - bytes], p->value, bytes); + total += bytes; + } + } + + EDITOR_PRINTF("%d bytes written\n", total); + return total; +} +int load1(bool apply, const char *buffer, unsigned buflen, array(property) arrp, unsigned skip_bytes) { + // iterate and load properties + unsigned cursor = 0, loaded = 0, limit = buflen; + + while( cursor <= skip_bytes ) + for each_array_ptr(arrp, property, p) { + unsigned bytes = 0; + /**/ if( p->type[0] == 'f') bytes = sizeof(float); + else if( p->type[0] == 'v') bytes = sizeof(vec3); + else if( p->type[0] == 'i') bytes = sizeof(int); + else if( p->type[0] == 'b') bytes = sizeof(bool); + + if( (cursor + bytes) > limit ) { + return -1; + } + + if( apply && cursor >= skip_bytes ) { + memcpy( p->value, &buffer[cursor], bytes); + loaded += bytes; + } + + cursor += bytes; + } + + EDITOR_PRINTF("%d bytes read, %d bytes loaded\n", cursor, loaded); + return cursor; +} + +int diff1( array(char) src, array(char) dst, array(char) *patch ) { // @testme + int slen = array_count(src); + int dlen = array_count(dst); + if( dlen != slen ) return -1; + + for( int i = 0; i < slen; ++i ) { + int diff = dst[i] - src[i]; + array_push(*patch, (char)diff); + } + + EDITOR_PRINTF("%d bytes diff\n", slen); + return slen; +} + +int patch1( array(char) src, array(char) dst, array(char) patch ) { // @testme + int slen = array_count(src); + int dlen = array_count(dst); + if( dlen != slen ) return -1; + + int plen = array_count(patch); + if( plen != dlen ) return -1; + + for( int i = 0; i < plen; ++i ) { + dst[i] += patch[i]; + } + + EDITOR_PRINTF("%d bytes patched\n", plen); + return plen; +} + +// syntax sugars for collections/containers +// #define bulk_load(obj_min,obj_max,objs,...) for( unsigned i = 0; i < array_count(objs); ++i ) { bool apply = obj_min >= i && i < obj_max; load1(apply, objs[i], __VA_ARGS__); } +// #define bulk_save(obj_min,obj_max,objs,...) for( unsigned i = 0; i < array_count(objs); ++i ) { bool apply = obj_min >= i && i < obj_max; save1(apply, objs[i], __VA_ARGS__); } + + +// state - retained mode + +typedef struct editor_state_t { + array(property) properties; + array(char) buffer; + array(vec2i) history; + unsigned cursor; +} editor_state_t; + +typedef map(char*, char*) editor_dict_t; + +typedef struct editor_call_t { + void* (*call)(); + unsigned bound; + void *vargs[4]; +} editor_call_t; + +typedef struct editor_module_t { + enum { + fn_init, + fn_load, + fn_tick, + fn_draw, + fn_aabb, // hitboxes + fn_debug, // call for debug ui (like loggers and sliders) + fn_save, + fn_quit, + + fn_num_, + } dummy; + + editor_call_t methods[fn_num_]; + +} editor_module_t; + +static map(void*, editor_state_t) editor_state; // world +static map(void*, array(void*)) editor_children; // groups for stacking, bvh and visibility +static map(void*, array(void*)) editor_children_tick; // groups for ticking +static map(void*, array(void*)) editor_children_draw; // groups for drawing +static map(void*, editor_module_t) editor_module; +static map(void*, editor_dict_t) editor_dicts; +static set(void*) editor_world; +static set(void*) editor_selection; // objects selected in scene + +void editor_init_states() { + do_once map_init_ptr(editor_state); + do_once map_init_ptr(editor_module); + do_once map_init_ptr(editor_children); + do_once map_init_ptr(editor_children_tick); + do_once map_init_ptr(editor_children_draw); + do_once map_init_ptr(editor_dicts); + do_once set_init_ptr(editor_world); + do_once set_init_ptr(editor_selection); +} + +// handle selection + +void editor_select(void *obj, bool multi) { + do_once editor_init_states(); + + editor_selected_obj = NULL; + + if(!multi) { + set_clear(editor_selection); + if( obj ) set_find_or_add(editor_selection, editor_selected_obj = obj); + } else { + if( !obj ) return; + bool on = !!set_find(editor_selection, obj); + if(on) set_erase(editor_selection, obj); + else set_find_or_add(editor_selection, editor_selected_obj = obj); + } +} +bool editor_is_selected(void *obj) { + do_once editor_init_states(); + + return !!set_find(editor_selection, obj); +} +void editor_select_none(void) { + editor_select(NULL, false); +} +void editor_select_all(void) { + editor_select_none(); + for each_set_ptr(editor_world, void*, o) { + void *obj = *o; + editor_select(obj, true); + } +} + +// obj/scene: load/save, undo/redo, + +bool editor_clear_redo(void *obj) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + if( ed->cursor >= array_count(ed->history) ) + return false; + + array_resize(ed->buffer, ed->history[ed->cursor].to); + array_resize(ed->history, ed->cursor + 1); + return true; +} + +bool editor_save_disk(const void *obj, const char *outfile) { + editor_state_t *ed = map_find_or_add(editor_state, (void*)obj, (editor_state_t){0}); + + static __thread array(char) buffer = 0; + array_resize(buffer, 0); // <-- reused as an optimization + + bool ok = 0; + if( save1(true, &buffer, ed->properties) > 0 ) { + ok = file_write(outfile, buffer, array_count(buffer)); + } + + ui_notify("Save", ok ? "OK" : ICON_MD_WARNING " Failed!"); + return ok; +} + +bool editor_load_disk(void *obj, const char *infile) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + int buflen; + char *buffer = file_load(infile, &buflen); + if( buffer && buflen ) { + if( load1(true, buffer, buflen, ed->properties, 0) > 0 ) { + return true; + } + } + + return false; +} + +bool editor_save_mem(void *obj) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + static array(char) buffer = 0; + array_resize(buffer, 0); + + // save + int bytes = save1(true, &buffer, ed->properties); + if( bytes <= 0 ) return false; + + // discard save if same size + same content (ie, no changes between this save and previous one) + if( array_count(ed->history) > 1 ) { + vec2i before = *array_back(ed->history); + if( bytes == (before.to - before.from) ) { + if( !memcmp(buffer, ed->buffer + array_count(ed->buffer) - bytes, bytes ) ) { + return false; // puts("discarding save..."); + } + } + } + +#if 0 + // discard redo + if( ed->cursor < array_count(ed->history) ) { + array_resize(ed->buffer, ed->history[ed->cursor].to); + array_resize(ed->history, ed->cursor + 1); + } +#else + editor_clear_redo(obj); +#endif + + // append + int checkpoint = array_count(ed->buffer); + array_resize(ed->buffer, array_count(ed->buffer) + bytes); + memcpy(ed->buffer + checkpoint, buffer, bytes); + + // proceed + array_push(ed->history, vec2i(checkpoint, array_count(ed->buffer))); + + // move cursor to latest + ed->cursor = array_count(ed->history) - 1; + + return true; +} + +bool editor_load_mem(void *obj) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + // load latest & update history + int slots = array_count(ed->history); + if( slots ) + if( load1(true, ed->buffer, array_count(ed->buffer), ed->properties, ed->history[slots - 1].from) > 0 ) + return ed->cursor = slots - 1, true; + + return false; +} + +bool editor_reset(void *obj) { // load first checkpoint + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + // load first slot + if( load1(true, ed->buffer, array_count(ed->buffer), ed->properties, ed->history[0].from) > 0 ) { + // discard redo + array_resize(ed->buffer, ed->history[0].to); + // update history + array_resize(ed->history, 1); + // move cursor to latest + ed->cursor = array_count(ed->history) - 1; + return true; + } + + return false; +} + +bool editor_undo(void *obj) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + // load previous & rewind history by -1 + if( ed->cursor > 0 ) + if( load1(true, ed->buffer, array_count(ed->buffer), ed->properties, ed->history[ed->cursor - 1].from) >= 0 ) + return ed->cursor -= 1, true; + + return false; +} + +bool editor_redo(void *obj) { + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + // load next & forward history by +1 + if( ed->cursor < (array_count(ed->history)-1) ) + if( load1(true, ed->buffer, array_count(ed->buffer), ed->properties, ed->history[ed->cursor + 1].from) >= 0 ) + return ed->cursor += 1, true; + + return false; +} + +bool editor_diff(const void *obj1, const void *obj2, array(char) patch) { // @todo + // @todo check: if both valid && both same type + return false; +} +bool editor_patch(void *obj1, array(char) patch) { // @todo + return false; +} + +// obj/module: persist + +char *editor_obj_intern(void *obj, const char *quark, const char *value) { + editor_init_states(); + + editor_dict_t *dict = map_find_or_add(editor_dicts, obj, 0); + if( *dict == 0 ) map_init_str(*dict); + + char **key = map_find_or_add_allocated_key(*dict, STRDUP(quark), 0); + if(*key) FREE(*key); + *key = STRDUP(value); + + return *key; +} + +char *editor_obj_string(const void *obj, const char *quark) { + editor_dict_t *dict = map_find_or_add(editor_dicts, (void*)obj, 0); + if( *dict == 0 ) map_init_str(*dict); + + char **key = map_find_or_add_allocated_key(*dict, STRDUP(quark), 0); + return *key ? *key : ""; +} + +// obj/module: hierarchy + +void editor_obj_childof_tick(void *obj, void *parent) { + array(void*) *found = map_find(editor_children_tick, parent); + if(!found) found = map_insert(editor_children_tick, parent, 0); + if( obj && obj != parent ) { // dont recurse + for( int i = 0; i < array_count(*found); ++i ) { + if( (*found)[i] == obj ) return; // child was already added + } + array_push(*found, obj); + } +} + +void editor_obj_childof_draw(void *obj, void *parent) { + array(void*) *found = map_find(editor_children_draw, parent); + if(!found) found = map_insert(editor_children_draw, parent, 0); + if( obj && obj != parent ) { // dont recurse + for( int i = 0; i < array_count(*found); ++i ) { + if( (*found)[i] == obj ) return; // child was already added + } + array_push(*found, obj); + } +} + +void editor_obj_childof(void *obj, void *parent) { + array(void*) *found = map_find(editor_children, parent); + if(!found) found = map_insert(editor_children, parent, 0); + if( obj && obj != parent ) { // dont recurse + for( int i = 0; i < array_count(*found); ++i ) { + if( (*found)[i] == obj ) return; // child was already added + } + array_push(*found, obj); + } +} + +// obj/module: methods + +typedef void* (*generic_method)(); + +void editor_obj_bind0(const void *obj, unsigned method, generic_method func ) { + do_once editor_init_states(); + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + m->call = func; + m->bound = 0; + + set_find_or_add(editor_world, (void*)obj); +} +void editor_obj_bind1(const void *obj, unsigned method, generic_method func, void *arg1 ) { + do_once editor_init_states(); + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + m->call = func; + m->vargs[0] = arg1; + m->bound = 1; + + set_find_or_add(editor_world, (void*)obj); +} +void editor_obj_bind2(const void *obj, unsigned method, generic_method func, void *arg1, void *arg2 ) { + do_once editor_init_states(); + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + m->call = func; + m->vargs[0] = arg1; + m->vargs[1] = arg2; + m->bound = 2; + + set_find_or_add(editor_world, (void*)obj); +} +void editor_obj_bind3(const void *obj, unsigned method, generic_method func, void *arg1, void *arg2, void *arg3 ) { + do_once editor_init_states(); + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + m->call = func; + m->vargs[0] = arg1; + m->vargs[1] = arg2; + m->vargs[2] = arg3; + m->bound = 3; + + set_find_or_add(editor_world, (void*)obj); +} +void editor_obj_bind4(const void *obj, unsigned method, generic_method func, void *arg1, void *arg2, void *arg3, void *arg4 ) { + do_once editor_init_states(); + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + m->call = func; + m->vargs[0] = arg1; + m->vargs[1] = arg2; + m->vargs[2] = arg3; + m->vargs[3] = arg4; + m->bound = 4; + + set_find_or_add(editor_world, (void*)obj); +} +void *editor_obj_call0(const void *obj, unsigned method ) { + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + if( !m->call ) return 0; + if( m->bound == 1 ) return (m->call)(obj, m->vargs[0]); + if( m->bound == 2 ) return (m->call)(obj, m->vargs[0], m->vargs[1]); + if( m->bound == 3 ) return (m->call)(obj, m->vargs[0], m->vargs[1], m->vargs[2]); + if( m->bound == 4 ) return (m->call)(obj, m->vargs[0], m->vargs[1], m->vargs[2], m->vargs[3]); + return (m->call)(obj); +} +void *editor_obj_call1(const void *obj, unsigned method, void *arg1 ) { + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + return m->call ? m->call(obj, arg1) : 0; +} +void *editor_obj_call2(const void *obj, unsigned method, void *arg1, void *arg2 ) { + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + return m->call ? m->call(obj, arg1, arg2) : 0; +} +void *editor_obj_call3(const void *obj, unsigned method, void *arg1, void *arg2, void *arg3 ) { + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + return m->call ? m->call(obj, arg1, arg2, arg3) : 0; +} +void *editor_obj_call4(const void *obj, unsigned method, void *arg1, void *arg2, void *arg3, void *arg4 ) { + editor_call_t *m = &(map_find_or_add(editor_module, (void*)obj, (editor_module_t){0})->methods[method]); + return m->call ? m->call(obj, arg1, arg2, arg3, arg4) : 0; +} + +// obj/module: ui/property + +void editor_obj_property(void *obj, void *value, const char *metas) { + do_once editor_init_states(); + + ASSERT( obj ); + ASSERT( value ); + ASSERT( metas ); + char *meta = va("%s", metas); + + struct property p = {0}; + + // parse tooltip, if present + for( char *tooltip = strstr(meta, " @"); tooltip; *tooltip = 0, tooltip = 0) { + p.hint = STRDUP(tooltip + 2); + } + + // parse metas, if present + for( char *metas = strstr(meta, "//"); metas; *metas = 0, metas = 0) { + for each_substring(metas + 2, " ", token) { + /**/ if(strbegi(token, "hint=")) token = token + 5 + strspn(token + 5, " "), p.hint = STRDUP(token); + else if(strbegi(token, "minv=")) token = token + 5 + strspn(token + 5, " "), p.minv = STRDUP(token); + else if(strbegi(token, "maxv=")) token = token + 5 + strspn(token + 5, " "), p.maxv = STRDUP(token); + } + } + + // parse declaration + unsigned field = ~0u; + for each_substring(meta, "={,}(); ", token) { + // next field + ++field; + // parse fields + /**/ if(field == 0) p.type = STRDUP(token); + else if(field == 1) { // either name or namespace.name + p.name = strchr(token, '.'); + if( !p.name ) p.name = STRDUP(token); + else p.name = STRDUP(p.name + 1), p.mark = STRDUP(token), *strchr(p.mark, '.') = '\0'; + } + else {} // any initialization values here + } + + // required fields + ASSERT(p.name); + ASSERT(p.type); + + // combine name+hint together + if( p.hint ) { + char *combined = va("%s@%s", p.name, p.hint); + FREE(p.hint), p.hint = 0; + strcatf(&p.hint, "%s", combined); + } else { + p.hint = p.name; + } + + // defaults + p.value = value; + + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + array_push( ed->properties, p ); +} + +void* editor_obj_get_property_by_index(const void *obj, unsigned property_no) { + editor_state_t *ed = map_find_or_add(editor_state, (void*)obj, (editor_state_t){0}); + return property_no < array_count(ed->properties) ? ed->properties[property_no].value : 0; +} + +void* editor_obj_get_property_by_name(const void *obj, const char *property_name) { + editor_state_t *ed = map_find_or_add(editor_state, (void*)obj, (editor_state_t){0}); + for each_array(ed->properties, struct property, p) { + if( !strmatchi(p.name, property_name) ) continue; + return p.value; + } + return 0; +} + +bool editor_obj_render_min_properties(const void *obj, const char *mask) { + editor_state_t *ed = map_find_or_add(editor_state, (void*)obj, (editor_state_t){0}); + + if(!mask) return false; + + const char *section = 0; + for each_array(ed->properties, struct property, p) { + if( p.mark ) { + if( section == 0 || strcmp(section, p.mark) ) { + if( section != 0 ) ui_separator(); + section = p.mark; + ui_label(va("*%s", section)); // '*' adds bold style in labels + } + } + + if( !strmatchi(p.name, mask) ) continue; + + /**/ if( p.type[0] == 'l') ui_label(p.hint); + else if( p.type[0] == 'f') ui_float(p.hint, p.value); + else if( p.type[0] == 'v') ui_float3(p.hint, p.value); + else if( p.type[0] == 'i') ui_int(p.hint, p.value); + else if( p.type[0] == 'b') ui_bool(p.hint, p.value); + } + return true; +} + +void editor_obj_render_max_properties(void *obj, const char *mask) { // headless, needs layout (window/panel) + const char *toolbar_text = va( + ICON_MD_LOOP ";" ICON_MD_SD_CARD ";" ICON_MD_UNDO ";" ICON_MD_REDO ";Save;Load;%s", + va(ICON_MD_BUG_REPORT "@Raise breakpoint (%sebugger detected).", has_debugger() ? "D":"No d")); // ICON_MD_FRONT_HAND + + int button = ui_toolbar(toolbar_text); + if( button ) { + if( button == 1 ) editor_key = key_load_disk; // key_reset; + if( button == 2 ) editor_key = key_save_disk; + if( button == 3 ) editor_key = key_undo; + if( button == 4 ) editor_key = key_redo; + if( button == 5 ) editor_key = key_save_disk; + if( button == 6 ) editor_key = key_load_disk; + if( button == 7 ) editor_key = key_debugger; + } + + ui_separator(); + + editor_obj_render_min_properties(obj, mask); + + ui_separator(); + ui_label("*Debug"); + + editor_state_t *ed = map_find_or_add(editor_state, obj, (editor_state_t){0}); + + static char *s = 0; if(s) *s = 0; + for( int i = 0; i < array_count(ed->history); ++i ) strcatf(&s, ",%s%d..%d", ed->cursor == i ? "->":"", (int)ed->history[i].from, (int)ed->history[i].to); + if(s) ui_label(va("Object Savepoints: %s", s+1)); + + ui_buffer("Object console", va("%s","(empty)"), 7+1); +} + +// main editor interface + +void editor_render_menubar() { + int alts = input(KEY_LALT) || input(KEY_RALT); // @todo: move to fwk.c + int ctrls = input(KEY_LCTRL) || input(KEY_RCTRL); // @todo: move to fwk.c + int shifts = input(KEY_LSHIFT) || input(KEY_RSHIFT); // @todo: move to fwk.c + int mods = alts || ctrls || shifts; // @todo: move to fwk.c + if( input_down(KEY_F5) ) editor_key = key_reload; + if( input_down(KEY_F11) ) editor_key = key_fullscreen; + if( input_down(KEY_PAUSE) ) editor_key = key_pause; + if( input_down(KEY_PRINT) ) editor_key = (mods ? key_recording : key_screenshot); + // if( input_down(KEY_W) && input_held(KEY_LCTRL) ) editor_key = key_quit; + + if( ctrls ) { + /**/ if( input_down(KEY_Z) ) editor_key = key_undo; + else if( input_down(KEY_Y) ) editor_key = key_redo; + else if( input_down(KEY_S) ) editor_key = key_save_disk; + else if( input_down(KEY_A) ) editor_select_all(); + else if( input_down(KEY_D) ) editor_select_none(); + } + + if( !editor_key /*&& !input_anykey()*/ && editor_selected_obj ) { + if( input_up(MOUSE_L) ) editor_key = key_save_mem; + if( input_down(MOUSE_R) ) ui_show("Properties", true); + #if 0 + { + vec2 dims = { 200, 400 }; + if( nk_tooltip_begin(ui_ctx, dims.w)) { + nk_layout_row_dynamic(ui_ctx, dims.h, 1); + editor_obj_render_min_properties(editor_selected_obj); + nk_tooltip_end(ui_ctx); + } + } + #endif + } + + // @fixme: send all editor keys to game? + // if( input_repeat(KEY_ESC, 300)) {} + // if( input_repeat(KEY_F1, 300)) {} + // etc... + + // menubar + + if( ui_menu( ICON_MD_SETTINGS "@Preferences;" + ICON_MD_G_TRANSLATE " Language;" + ICON_MD_FACE " Profile;" // editor account, but also fake profile and 1st party credentials + ICON_MD_MESSAGE " Social;" + ICON_MD_ROCKET_LAUNCH " Game;" // + ICON_MD_KEYBOARD " Keyboard;" + ICON_MD_MOUSE " Mouse;" + ICON_MD_GAMEPAD " Gamepad;" + ICON_MD_MONITOR " Display;" // @todo: RENDER settings, AUDIO settings + ICON_MD_WIFI " Network;" + ICON_MD_SAVINGS " Budget;" // mem,gfx,net,hdd,... also logging + ICON_MD_CREATE_NEW_FOLDER " Folders;" // including project folders + ICON_MD_EXTENSION " Plugins;" // including VCS + ICON_MD_REPLAY " Restart;" + ICON_MD_CLOSE " Quit;" + "-" ICON_MD_RECYCLING " Reset all preferences;" ICON_MD_SAVE_AS " Save all preferences" + ) ) { + if( ui_item() == 3 ) {} // key mappings + if( ui_item() == 4 ) {} // sensitivity, invert xylrw + if( ui_item() == 5 ) {} // sensitivity, invert xy,ab, deadzones + if( ui_item() == 7 ) {} // name,email,icon,link,github + if( ui_item() == 13) editor_key = key_reload; + if( ui_item() == 14) editor_key = key_quit; + } + + if( ui_menu( window_has_pause() ? ICON_MD_PLAY_ARROW "@Tap to Play Game" : ICON_MD_PAUSE "@Tap to Pause Game" )) editor_key = key_pause; + if( ui_menu( ICON_MD_STOP "@Stop game" )) editor_key = key_stop; + if( ui_menu( ICON_MD_CLOSE "@Close game" ) ) {} + static char game_args[16] = "--game-args"; // @fixme @todo remove '_' special char to signal that ui_menu() is writeable (inputbox) + if( ui_menu_editbox( game_args, 16 ) ) {} + + // ICON_MD_TROUBLESHOOT -> PROFILER + // ICON_MD_SCHEMA -> GRAPHNODES + // ICON_MD_ACCOUNT_TREE -> GRAPHNODES + // ICON_MD_TIPS_AND_UPDATES -> BULB + // if( ui_menu( ICON_MD_MENU )) {} + + if( ui_menu( ICON_MD_FOLDER_SPECIAL "@Content browser" )) editor_key = key_browser; + if( ui_menu( va(ICON_MD_VIEW_IN_AR " %d/%d@World outliner", set_count(editor_selection), map_count(editor_state) ))) editor_key = key_outliner; + + if( ui_menu( va(ICON_MD_BUILD "@Build game"))) ui_notify("Build", ICON_MD_WARNING " Not implemented."); + + if( ui_menu( ICON_MD_PHOTO_CAMERA "@Take Screenshot" )) editor_key = key_screenshot; // MD_SCREENSHOT_MONITOR + if( ui_menu( record_active() ? ICON_MD_VIDEOCAM_OFF "@Stop video recording" : ICON_MD_VIDEOCAM "@Start video recording" )) { if(record_active()) record_stop(); else editor_key = key_recording; } + if( ui_menu( editor_gamepad ? ICON_MD_VIDEOGAME_ASSET "@Gamepad is enabled. Tap to disable." : ICON_MD_VIDEOGAME_ASSET_OFF "@Gamepad is disabled. Tap to enable." )) editor_key = key_gamepad; + if( ui_menu( audio_volume_master(-1) > 0 ? ICON_MD_VOLUME_UP "@Audio is enabled. Tap to mute." : ICON_MD_VOLUME_OFF "@Audio is muted. Tap to enable." )) editor_key = key_mute; + if( ui_menu( window_has_fullscreen() ? ICON_MD_FULLSCREEN_EXIT "@Fullscreen. Tap to go Windowed." : ICON_MD_FULLSCREEN "@Windowed. Tap to go Fullscreen." )) editor_key = key_fullscreen; + + if( ui_menu( editor_ddraw ? ICON_MD_IMAGE_SEARCH "@Debug renderer. Tap to go Retail Renderer." : ICON_MD_INSERT_PHOTO "@Retail renderer. Tap to go Debug Renderer." )) editor_key = key_ddraw; // ICON_MD_ADD_PHOTO_ALTERNATE + if( ui_menu( editor_lit ? ICON_MD_LIGHTBULB "@Lit. Tap to disable lighting." : ICON_MD_LIGHTBULB_OUTLINE "@Unlit. Tap to enable lighting." )) editor_key = key_lit; + + // logic: either plug icon (power saving off) or one of the following ones (power saving on): + // if 0% batt (no batt): battery alert + // if discharging: battery levels [alert,0..6,full] + // if charging: battery charging + int battery_read = app_battery(); + int battery_level = abs(battery_read); + int battery_discharging = battery_read < 0 && battery_level < 100; + const char *battery_levels[9] = { // @todo: remap [7%..100%] -> [0..1] ? + ICON_MD_BATTERY_ALERT, + ICON_MD_BATTERY_0_BAR,ICON_MD_BATTERY_1_BAR, + ICON_MD_BATTERY_2_BAR,ICON_MD_BATTERY_3_BAR, + ICON_MD_BATTERY_4_BAR,ICON_MD_BATTERY_5_BAR, + ICON_MD_BATTERY_6_BAR,ICON_MD_BATTERY_FULL, + }; + if( ui_menu( !editor_power_saving ? ICON_MD_POWER"@Full power. Tap to save power." : + va("%s@Power saving. Tap to go full power. %3d%% battery.", + battery_read == 0 ? ICON_MD_BATTERY_ALERT : + battery_discharging ? battery_levels[(int)((9-1)*clampf(battery_level/100.f,0,1))] : + ICON_MD_BATTERY_CHARGING_FULL, battery_level) )) + editor_key = key_battery; + + // @todo: combine-in-1? cycle mem -> cpu/profiler -> network mon -> debugger + if( ui_menu(va(ICON_MD_SIGNAL_CELLULAR_ALT " 0/0KiB" ))) {} // SIGNAL_CELLULAR_1_BAR SIGNAL_CELLULAR_2_BAR + if( ui_menu(va(ICON_MD_STORAGE " %s", xstats() ))) {} // 012/136MB + if( ui_menu(va(ICON_MD_SPEED " %5.2f/%d", window_fps(), (int)window_fps_target()))) editor_key = key_profiler; // 012/136MB + + // @todo: alarm/snooze, chrono, time (calendar?) + { + static double base = 0, tap = 0, delta = 0, enabled = 0; + double timer = base + delta; + if( ui_menu( !enabled ? + va(ICON_MD_TODAY "%02d:%02d@Tap to start chrono.", (int)((date() / 10000) % 100), (int)((date() / 100) % 100)) + : + va(ICON_MD_TIMELAPSE "%02dh:%02dm:%02ds:%02df@Tap to reset chrono.",((int)(timer/3600))%24,((int)(timer/60))%60,((int)timer)%60,(int)((timer - (int)timer)*window_fps_target())))) { + base = 0, tap = time_ss(), delta = 0; + enabled = 1; + } + if( editor_key == key_stop ) enabled = 0; + if( enabled ) { + if( !window_has_pause() ) delta = time_ss() - tap; + else base += delta, tap = time_ss(), delta = 0; + } + } + + for each_map_ptr(editor_state, void *, o, editor_state_t, ed) { + profile_incstat("Editor.num_objects", +1); + + void *obj = *o; + + // auto-load from disk during init. @fixme kvdb database + if( array_count(ed->history) == 0 ) + if( editor_load_disk(obj, editor_obj_string(obj, ".path")) ) + {} + + // auto-save in-mem during first edit + if( array_count(ed->history) == 0 ) + editor_save_mem(obj); + + // @todo: continue if obj not found in selection set + if( obj != editor_selected_obj ) + continue; + + if( editor_key == key_debugger ) { breakpoint("User requested breakpoint on this object"); } + if( editor_key == key_reset ) { const char *ok = editor_reset(obj) ? "ok" : "err"; EDITOR_PRINTF("reset: %s\n", ok); } + if( editor_key == key_save_mem ) { const char *ok = editor_save_mem(obj) ? "ok" : "err"; EDITOR_PRINTF("mem saved: %s\n", ok); } + if( editor_key == key_undo ) { const char *ok = editor_undo(obj) ? "ok" : "err"; EDITOR_PRINTF("undo: %s\n", ok); } + if( editor_key == key_redo ) { const char *ok = editor_redo(obj) ? "ok" : "err"; EDITOR_PRINTF("redo: %s\n", ok); } + if( editor_key == key_save_disk ) { const char *ok = editor_save_disk(obj, editor_obj_string(obj, ".path")) ? "ok" : "err"; EDITOR_PRINTF("save: %s\n", ok); } + if( editor_key == key_load_disk ) { const char *ok = editor_load_disk(obj, editor_obj_string(obj, ".path")) ? "ok" : "err"; EDITOR_PRINTF("load: %s\n", ok); } + } + + char *name; + switch( editor_key ) { + default: + break; case key_quit: record_stop(), exit(0); + break; case key_stop: window_pause(1); + break; case key_mute: audio_volume_master( 1 ^ !!audio_volume_master(-1) ); + break; case key_pause: window_pause( window_has_pause() ^ 1 ); + break; case key_reload: window_reload(); + break; case key_battery: editor_power_saving ^= 1; + break; case key_browser: ui_show("File Browser", ui_visible("File Browser") ^ true); + break; case key_outliner: ui_show("Outliner", ui_visible("Outliner") ^ true); + break; case key_recording: name = file_counter(va("%s.mp4",app_name())), window_record(name), ui_notify(va("Video capturing: %s", name), date_string()); + break; case key_screenshot: name = file_counter(va("%s.png",app_name())), window_screenshot(name), ui_notify(va("Screenshot: %s", name), date_string()); + break; case key_profiler: ui_show("Profiler", profile_enable(ui_visible("Profiler") ^ true)); + break; case key_fullscreen: record_stop(), window_fullscreen( window_has_fullscreen() ^ 1 ); // framebuffer resizing corrupts video stream, so stop any recording beforehand + break; case key_gamepad: editor_gamepad ^= 1; + break; case key_lit: editor_lit ^= 1; + break; case key_ddraw: editor_ddraw ^= 1; + } +} + +int do_context_cmd = 0; +void *do_context_obj = 0; + +void editor_obj_render_properties_recursively(void *obj, const char *mask) { + array(void*) *found = map_find(editor_children, obj); + int num_subobjects = found ? array_count(*found) : 0; + + char *name = editor_obj_string(obj,".name"); name = name[0] ? name : va("%p", obj); + char *title = va("%s%s/ (%d)", + editor_is_selected(obj) ? ICON_MD_CHECK_BOX : ICON_MD_CHECK_BOX_OUTLINE_BLANK, name, num_subobjects); +// if( !strmatchi(title, mask) ) return; + char *id = va("LVL%p",obj); + + int clicked_or_toggled, open; // 1|clicked, 2|toggled + for( int p = (open = ui_collapse(title, id)), dummy = (clicked_or_toggled = ui_collapse_clicked()); p; ui_collapse_end(), p = 0) { + + // contextual menu (open) + if( ui_contextual() ) { + if( ui_button_transparent("C"); + if( choice ) printf("%d\n", choice); + + for( int i = 0; i < num_subobjects; ++i ) { + editor_tick_objs_recursively((*found)[i], flags); + } + } + } else { + for( int i = 0; i < num_subobjects; ++i ) { + editor_tick_objs_recursively((*found)[i], flags); + } + } +} + +void editor_draw_objs_recursively(void *obj, unsigned flags) { + array(void*) *found = map_find(editor_children_draw, obj); + int num_subobjects = found ? array_count(*found) : 0; + + if( flags & DRAW_ENABLED ) editor_obj_call0(obj, fn_draw); + + if( flags & DRAW_DO_UI ) { + char *name = editor_obj_string(obj,".name"); name = name[0] ? name : va("%p", obj); + char *title = va("%s%s/ (%d)", editor_is_selected(obj) ? ICON_MD_CHECK_BOX : ICON_MD_CHECK_BOX_OUTLINE_BLANK, name, num_subobjects); + // if( !strmatchi(title, mask) ) return; + char *id = va("GPU%p",obj); + + int clicked; + for( int p = ui_collapse(title, id), dummy = (clicked = ui_collapse_clicked()); p; ui_collapse_end(), p = 0) { + + int choice = ui_submenu("D"); + if( choice ) printf("%d\n", choice); + + for( int i = 0; i < num_subobjects; ++i ) { + editor_draw_objs_recursively((*found)[i], flags); + } + } + + } else { + for( int i = 0; i < num_subobjects; ++i ) { + editor_draw_objs_recursively((*found)[i], flags); + } + } +} + +void editor_render_windows() { + // content browser + if( ui_window("File Browser", 0) ) { + const char *file = 0; + if( ui_browse(&file, NULL) ) { + const char *sep = ifdef(win32, "\"", "'"); + app_exec(va("%s %s%s%s", ifdef(win32, "start \"\"", ifdef(osx, "open", "xdg-open")), sep, file, sep)); + } + ui_window_end(); + } + + // console/terminal window + if( 0 && ui_window("Console", 0) ) { + ui_console(); + ui_window_end(); + } + + // Scene/nodes + if( ui_window("Outliner", 0) ) { + // @todo: keys up,down,left,right -> tree nav + // @todo: tab -> cycle next node of matching highlighted type + + static int do_filter = 0; + + int choice = ui_toolbar(ICON_MD_SEARCH"@Filter;"ICON_MD_UPLOAD"@Load;"ICON_MD_DOWNLOAD"@Save;"ICON_MD_REFRESH"@Reset;"ICON_MD_UNDO"@Undo;"ICON_MD_REDO"@Redo;"); + if( choice == 1 ) do_filter ^= 1; + + static char filter[128] = {0}; + if( do_filter ) { + ui_buffer(ICON_MD_SEARCH " Filter", filter, 128); + } else { + filter[0] = '\0'; + } + char *mask = filter[0] ? va("*%s*", filter) : "*"; + +#if 0 + static unsigned tabs = 0xFF; + int choice = ui_toolbar( + "LV@Level tree: hierarchical logic datas used when ticking game.;" + "RN@Rendering tree: hierarchical rendering datas used when drawing game.;" + "VS@Visibility tree: hierarchical visibility datas used when ticking game and editor. Also collisions.;" + "ST@Streaming tree: hierarchical streaming datas used when streaming content off disk.;" + "PS@Persist tree: hierarchical storage datas within different game sessions.;" + "PR@Prefabs tree: hierarchical datas of prefabs definitions.;" + "ED@Editor tree: hierarchical datas used when ticking editor.;" + ); +#endif + + for( int c = ui_collapse(ICON_MD_FOLDER_SPECIAL " Art/", "ART"); c; ui_collapse_end(), c = 0) { + static const char *file; + static bool show_browser = 1; + if( ui_browse(&file, &show_browser) ) { + app_exec(va("%s %s", ifdef(win32, "start", ifdef(osx, "open", "xdg-open")), file)); + //puts(file); + } + } + + // + for( int c = ui_collapse(va(ICON_MD_FACTORY " Prefabs/ (%d)", map_count(editor_state)), "PRF"); c; ui_collapse_end(), c = 0) + for each_map_ptr(editor_state, void*, o, editor_state_t, ed) { + void *k = *o; + for( int p = ui_collapse(va("%s",strrchr(editor_obj_string(k,".path"),'/')+1), va("PF#%p",k)); p; ui_collapse_end(), p = 0) { + editor_obj_render_min_properties(k, mask); + } + } + + // dynamic/static bounds: depth + bounds + visibility + do_context_cmd = 0; + do_context_obj = 0; + for( int c = ui_collapse(va(ICON_MD_ACCOUNT_TREE " Levels/ (%d)", map_count(editor_children)), "LVL"); c; ui_collapse_end(), c = 0) + for each_map_ptr(editor_children, void*, o, array(void*), objs) { + void *k = *o; + editor_obj_render_properties_recursively(k, mask); + } + if( do_context_cmd == cc4(list) && do_context_obj ) { + printf("list [%p]\n", do_context_obj); + } + // draw: depth + state (alpha0=off) + // @fixme: make it a tree + for( int c = ui_collapse(va(ICON_MD_PALETTE " Rendering/ (%d)", map_count(editor_children_draw)), "GPU"); c; ui_collapse_end(), c = 0) + for each_map_ptr(editor_children_draw, void*, o, array(void*), objs) { + void *k = *o; + editor_draw_objs_recursively(k, DRAW_DO_UI); + } + // tick: depth + rate (00=off) --> logic + // @todo: physics tick group? anim tick group? any other tick group? + // @fixme: make it a tree + for( int c = ui_collapse(va(ICON_MD_FLAG " Ticking/ (%d)", map_count(editor_children_tick)), "CPU"); c; ui_collapse_end(), c = 0) + for each_map_ptr(editor_children_tick, void*, o, array(void*), objs) { + void *k = *o; + editor_tick_objs_recursively(k, TICK_DO_UI); + } + // init/quit: depth + prio + // @fixme: make it a tree + for( int c = ui_collapse(ICON_MD_CLOUD " Streaming/", "BVH"); c; ui_collapse_end(), c = 0) {} + // save/load: depth + savetomem?yn + savetodisk?yn + quant + lossy/lossless + // @fixme: make it a tree + for( int c = ui_collapse(va(ICON_MD_SD_CARD " Storage/ (%d)", map_count(editor_dicts)), "DSK"); c; ui_collapse_end(), c = 0) + for each_map_ptr(editor_dicts, void*, o, editor_dict_t, d) { + void *k = *o; + for( int p = ui_collapse(editor_obj_string(k,".name"), va("DSK%p",k)); p; ui_collapse_end(), p = 0) { + for each_map(*d, char*, s, char *, v) { + ui_label(va("%s: %s", s, v)); + } + } + } + + // others + for( int c = ui_collapse(ICON_MD_PRECISION_MANUFACTURING " Editors/", "EDT"); c; ui_collapse_end(), c = 0) { + // @todo: add settings here as well? + } + + for( int c = ui_collapse(ICON_MD_INFO " Help", "NFO"); c; ui_collapse_end(), c = 0) { + ui_label("=*General"); + ui_label2("*ESC", ">Editor on/off"); + ui_label2("*F11", ">Fullscreen on/off"); + ui_label2("*F5", ">Refresh"); + ui_separator(); + ui_label("=*Edit"); + ui_label2("*^Z, ^Y", ">Undo, Redo"); + ui_label2("*^X, ^C, ^V", ">Cut, Copy, Paste"); + ui_label2("*^S, ^L, ^R", ">Save, Load*, Restart*"); + ui_separator(); + ui_label("=*Select"); + ui_label2("*LMB, ^A, ^D", ">Select, All, None"); + ui_label2("*RMB", ">Contextual menu*"); + ui_label2("*SPACE@Cycle transform gizmo: position, rotation, scale.", ">Cycle transform gizmo"); + ui_separator(); + ui_label("=*Camera"); + ui_label2("*Q,E,C", ">Camera elevation"); + ui_label2("*W,A,S,D", ">Camera move"); + ui_label2("*LMB/RMB+drag", ">Camera view"); + ui_label2("*WMB", ">Camera speed"); + } + + ui_window_end(); + } + + // UI properties + if( ui_window("Properties", NULL) ) { + if( editor_selected_obj ) + editor_obj_render_max_properties(editor_selected_obj, "*"); + ui_window_end(); + } + + // Icon browser + if( ui_window("Icon Palette", 0 )) { + static const char *icons[] = { + #define ICON(x) ICON_MD_##x + #include "editor" + }; + static const char *titles[] = { + #define ICON(x) #x + #include "editor" + }; + + for( int i = 0, cols = 8; i < countof(icons); i += cols ) { + char buf[256], *p = buf; + for( int j = i, jmax = mini(i+cols, countof(icons)); j < jmax; ++j ) p += sprintf(p, "%s%03d@%s;", icons[j], j, titles[j]); + ui_toolbar(buf); + } + + ui_window_end(); + } +} + +ray *editor_pickup() { +// if(!window_has_cursor()) return NULL; + + // pick entity + bool any_active = ui_active() || ui_hover() || gizmo_active() || gizmo_hover() || input_touch_active(); + if( editor_enabled && !any_active && input_down(MOUSE_L) ) { + editor_mouse = vec2(input(MOUSE_X), input(MOUSE_Y)); + vec3 out = editor_pick(editor_mouse.x, editor_mouse.y); // unprj 2d as 3d coord + vec3 from = camera_get_active()->position, to = out; + static ray last_ray; + last_ray = ray(from, to); + return &last_ray; + } + + return 0; +} + +static +void editor_init() { + // enable outlines + do_once fx_load("editor/art/fx/fxOutline.fs"); + do_once fx_enable(0, 1); + + // init editor + do_once editor_init_states(); + do_once editor_init_variables(); +} + +bool editor() { + do_once editor_init(); + + // timing + editor_dt = window_delta() * !window_has_pause(); if(editor_dt > 1/60.f) editor_dt = 1/60.f; + editor_t += editor_dt; + + // enabled? + if( input_up(KEY_ESC) ) editor_enabled ^= 1; // editor on/off + if( !editor_enabled ) return false; + + // rendering + logic + editor_key = 0; + editor_render_windows(); + editor_render_menubar(); // must be last + + // adaptive framerate + int app_on_background = !window_has_focus(); + int hz = app_on_background ? editor_hz_low : editor_power_saving ? editor_hz_mid : editor_hz; + window_fps_lock( hz < 5 ? 5 : hz ); + + return true; +} + +void editor_camera_fps(void) { + static camera_t cam = {0}; + cam = *camera_get_active(); + + vec3 move = {0}; + vec2 view = {0}; + + // show/hide cursor + bool dragging = input(MOUSE_L) || input(MOUSE_M) || input(MOUSE_R); + bool any_active = ui_active() || ui_hover() || gizmo_active() || input_touch_active(); + if( any_active ) dragging = false; + window_cursor( !dragging ); + + // keyboard/mouse + if( dragging ) cam.speed = clampf(cam.speed + input_diff(MOUSE_W) / 10, 0.05f, 5.0f); + vec3 wasdec = scale3(vec3(input(KEY_D)-input(KEY_A),input(KEY_E)-(input(KEY_Q)||input(KEY_C)),input(KEY_W)-input(KEY_S)), cam.speed * !any_active); + vec2 mouse = scale2(vec2(input_diff(MOUSE_X), -input_diff(MOUSE_Y)), 0.2f * dragging * !any_active); + if( !input(KEY_LCTRL) && !input(KEY_RCTRL) ) // invalidate keys if pressing ctrl (ie, when saving CTRL-S) + move = add3(move, wasdec); + view = add2(view, mouse); + + // gamepad + if(0) { + vec2 filtered_lpad = input_filter_deadzone(input2(GAMEPAD_LPAD), 0.15f /*15% deadzone*/); + vec2 filtered_rpad = input_filter_deadzone(input2(GAMEPAD_RPAD), 0.15f /*15% deadzone*/); + vec3 gamepad_move = scale3(vec3(filtered_lpad.x, input(GAMEPAD_LT) - input(GAMEPAD_RT), filtered_lpad.y), 1.0f); + vec2 gamepad_view = scale2(filtered_rpad, 1.0f); + move = add3(move, gamepad_move); + view = add2(view, gamepad_view); + } + + // multi-touch + vec2 touch_move = input_touch_delta_from_origin(0, 0.0125f /*sensitivityFwd*/); // button #0 (left border) + vec2 touch_view = input_touch_delta(1, 0.125f /*sensitivityRot*/); // button #1 (right border) + move = add3(move, vec3(touch_move.x, 0, -touch_move.y)); + view = add2(view, vec2(touch_view.x, -touch_view.y)); + + // apply inputs + camera_move(&cam, move.x,move.y,move.z); + camera_fps(&cam, view.x,view.y); +} + +// sugars +static void *editor_with = 0; +#define editor_obj(x) (editor_with = (x)) +#define editor_obj_childof(...) editor_obj_childof(editor_with, __VA_ARGS__) +#define editor_obj_childof_tick(...) editor_obj_childof_tick(editor_with, __VA_ARGS__) +#define editor_obj_childof_draw(...) editor_obj_childof_draw(editor_with, __VA_ARGS__) +#define editor_obj_intern(...) editor_obj_intern(editor_with, __VA_ARGS__) +#define editor_obj_property(...) editor_obj_property(editor_with, __VA_ARGS__) +#define editor_obj_bind1(...) editor_obj_bind1(editor_with, __VA_ARGS__) +#define editor_obj_bind2(...) editor_obj_bind2(editor_with, __VA_ARGS__) +#define editor_obj_bind3(...) editor_obj_bind3(editor_with, __VA_ARGS__) +#define editor_obj_bind4(...) editor_obj_bind4(editor_with, __VA_ARGS__) + + +// my game + +void* mygrid_draw(void *singleton, float *ground_size) { + ddraw_ontop_push(0); + ddraw_grid(*ground_size); + ddraw_ontop_pop(); + ddraw_flush(); + return 0; +} +void* mymodel_draw(model_t *m, float pivot[16]) { + camera_t *cam = camera_get_active(); + model_render(*m, cam->proj, cam->view, pivot, 0); + return 0; +} +void* mymodel_tick(model_t *m, float pivot[16], vec3 *p, vec3 *r, vec3 *s) { + rotationq44(pivot, eulerq(*r)); + scale44(pivot, s->x,s->y,s->z); + relocate44(pivot,p->x,p->y,p->z); + return 0; +} +void* mymodel_aabb(model_t *m, float pivot[16]) { + static __thread struct aabb aabb[64]; + static __thread int counter = 0; counter = (counter + 1) % 64; + aabb[counter] = model_aabb(*m, pivot); + return &aabb[counter]; +} + + +int main() { + // 80% window, MSAAx2 flag + window_create(80, WINDOW_MSAA2); + window_title("Editor " EDITOR_VERSION " (wip)"); + window_icon("logo.png"); + + // @fixme + camera_t x = camera(); + + // config ground floor + float ground_size = 0; + editor_obj(&ground_size); + editor_obj_intern(".name", "ground"); + editor_obj_intern(".path", editor_path("ground.ini")); + editor_obj_property(&ground_size, "float Size"); + editor_obj_bind1(fn_draw, mygrid_draw, &ground_size); + + // config 3d model #1 + mat44 witch_pivot; + vec3 witch_p = {-5,0,-5}, witch_r={-180,180,0}, witch_s={0.1,-0.1,0.1}; + model_t witch = model("witch/witch.obj", 0); + model_set_texture(witch, texture("witch/witch_diffuse.tga.png", 0)); + + editor_obj(&witch); + editor_obj_childof(&ground_size); + editor_obj_childof_tick(&ground_size); + editor_obj_childof_draw(&ground_size); + editor_obj_intern(".name", "witch"); + editor_obj_intern(".path", editor_path("witch.ini")); + editor_obj_property(&witch_p, "vec3 Position"); // property #0 + editor_obj_property(&witch_r, "vec3 Rotation"); // property #1 + editor_obj_property(&witch_s, "vec3 Scale"); // property #2 + editor_obj_bind1(fn_draw, mymodel_draw, witch_pivot); + editor_obj_bind1(fn_aabb, mymodel_aabb, witch_pivot); + editor_obj_bind4(fn_tick, mymodel_tick, witch_pivot, &witch_p, &witch_r, &witch_s); + + // config 3d model #2 + mat44 girl_pivot; id44(girl_pivot); + model_t girl = model("kgirl/kgirls01.fbx", 0); + vec3 girl_p = {0,0,0}, girl_r = {270,0,0}, girl_s = {2,2,2}; + + editor_obj(&girl); + editor_obj_childof(&ground_size); + editor_obj_childof_tick(&ground_size); + editor_obj_childof_draw(&ground_size); + editor_obj_intern(".name", "girl"); + editor_obj_intern(".path", editor_path("girl.ini")); + editor_obj_property(&girl_p, "vec3 Transform.Position; // @Position in world units"); + editor_obj_property(&girl_r, "vec3 Transform.Rotation; // @Rotation in degrees"); + editor_obj_property(&girl_s, "vec3 Transform.Scale; // @Scale factor (decimal)"); + editor_obj_bind1(fn_draw, mymodel_draw, girl_pivot); + editor_obj_bind1(fn_aabb, mymodel_aabb, girl_pivot); + editor_obj_bind4(fn_tick, mymodel_tick, girl_pivot, &girl_p, &girl_r, &girl_s); + + // meta(&girl_frame, "float Animation.Frame; // @Animation frame"); + + editor_select(&girl, false); + + ui_notify("Hint", "Keys I/J/K/L + Z/X to control the girl"); + + // editor loop + while( window_swap() ) { + + // editor tick + profile("Editor") { + editor(); + //ui_demo(); + } + + // fps camera + if( /*editor_attached ||*/ editor_enabled ) { + profile("Editor.Camera") { + editor_camera_fps(); + } + } else { + profile("Game.Camera") { + camera_t *cam = camera_get_active(); + + static vec3 source; + do_once source = cam->position; + + vec3 target = add3(girl_p, vec3(0,10,0)); + target = add3(target, scale3(norm3(sub3(source, target)), 10.0)); + source = mix3(source, target, 1-0.99f); + + camera_teleport(cam, source); + camera_lookat(cam, vec3(girl_p.x,0,girl_p.z)); + + // @todo: orbit cam w/ right pad + } + } + + double GAME_JUMP_DOWN = input_down(KEY_Z); + double GAME_FIRE_DOWN = input_down(KEY_X); + double GAME_JUMP = input(KEY_Z); + double GAME_FIRE = input(KEY_X); + double GAME_LEFT = input(KEY_J); + double GAME_RIGHT = input(KEY_L); + double GAME_UP = input(KEY_I); + double GAME_DOWN = input(KEY_K); + double GAME_AXISX = input(KEY_L) - input(KEY_J); + double GAME_AXISY = input(KEY_I) - input(KEY_K); + + if( editor_gamepad && !input_anykey() ) { + if( input(GAMEPAD_CONNECTED) ) { + vec2 filtered_lpad = input_filter_deadzone(input2(GAMEPAD_LPAD), 0.15f /*15% deadzone*/); + GAME_JUMP_DOWN = input_down(GAMEPAD_A); + GAME_FIRE_DOWN = input_down(GAMEPAD_B) || input_down(GAMEPAD_X) || input_down(GAMEPAD_Y); + GAME_JUMP = input(GAMEPAD_A); + GAME_FIRE = input(GAMEPAD_B) || input(GAMEPAD_X) || input(GAMEPAD_Y); + GAME_AXISX = filtered_lpad.x; + GAME_AXISY = filtered_lpad.y; + } + } + + profile("Game.Animate scene") if( editor_delta() ) { + float delta = editor_delta() * 30; // 30fps anim + + // animate girl + girl.curframe = model_animate(girl, girl.curframe + delta); + + // jump controller: jump duration=1.5s, jump height=6 units, anims (expo->circ) + float jump_delta = 1.0; + static double jump_timer = 0, jump_ss = 1.5, jump_h = 6; + if( GAME_JUMP_DOWN ) if( jump_timer == 0 ) jump_timer = editor_ss(); + jump_delta = clampf(editor_ss() - jump_timer, 0, jump_ss) * (1.0/jump_ss); + if( jump_delta >= 1 ) { jump_timer = 0; } + float y = ease_ping_pong( jump_delta, ease_out_expo, ease_out_circ); + girl_p.y = y * jump_h; + + // punch controller + float punch_delta = 1; + if( jump_delta >= 1 ) { + static vec3 origin; + static double punch_timer = 0, punch_ss = 0.5; + if( GAME_FIRE_DOWN ) if( punch_timer == 0 ) punch_timer = editor_ss(), origin = girl_p; + punch_delta = clampf(editor_ss() - punch_timer, 0, punch_ss) * (1.0/punch_ss); + if( punch_delta >= 1 ) { punch_timer = 0; } + else { + float x = ease_out_expo( punch_delta ); + vec3 fwd = rotate3q(vec3(0,0,1), eulerq(vec3(girl_r.x - 170,girl_r.y,girl_r.z))); + vec3 mix = mix3(girl_p, add3(origin,scale3(fwd,x*2)), x); + girl_p.x = mix.x, girl_p.z = mix.z; + } + } + + int modern_controller = 1; + int running = 0; + + // girl controller + + // locomotion vars + float speed = 0.2f * delta; + float yaw_boost = GAME_AXISY > 0 ? 1.0 : 1.75; + if(punch_delta < 1) yaw_boost = 0.0; // if firing... + else if(punch_delta <= 0.1) yaw_boost = 4.0; // unless initial punch chaining, extra yaw + + // old fashioned locomotion controller (boat controller) + if(!modern_controller) { + running = GAME_AXISY > 0; + + girl_r.x -= 170; + quat q = eulerq(girl_r); // += custom.pivot + vec3 rgt = rotate3q(vec3(1,0,0), q); + vec3 up = rotate3q(vec3(0,1,0), q); + vec3 fwd = rotate3q(vec3(0,0,1), q); + vec3 dir = scale3(fwd, speed * GAME_AXISY * (GAME_AXISY > 0 ? 2.0 : 0.5)); + girl_r.x += speed * 20.0 * yaw_boost * GAME_AXISX; // yaw + girl_p = add3(girl_p, dir); + girl_r.x += 170; + } + + // modern locomotion controller (mario 3d) + if(modern_controller) { + running = GAME_AXISX != 0 || GAME_AXISY != 0; + + camera_t *cam = camera_get_active(); + vec3 fwd = sub3(girl_p, cam->position); fwd.y = 0; fwd = norm3(fwd); + vec3 rgt = norm3(cross3(fwd, vec3(0,1,0))); + + // target + vec3 dir = add3( + scale3(fwd, GAME_AXISY), + scale3(rgt, GAME_AXISX) + ); dir.y = 0; dir = norm3(dir); + + // smoothing + static vec3 olddir; do_once olddir = dir; + dir = mix3(dir, olddir, 1 - (yaw_boost / 4.0) * 0.85); + olddir = dir; + + // vis + // ddraw_arrow(girl_p, add3(girl_p,scale3(dir,10))); + + // apply direction + girl_p = add3(girl_p, scale3(dir, speed * 2)); + + // apply rotation + { + girl_r.x -= 170; + quat q = eulerq(girl_r); + vec3 fwdg = rotate3q(vec3(0,0,1), q); + girl_r.x += 170; + + //float cosAngle = dot3(dir,fwdg); + //float angle = acos(cosAngle) * TO_DEG; + float angle = TO_DEG * ( atan2(fwdg.z, fwdg.x) - atan2(dir.z, dir.x)); + + if( !isnan(angle) ) { + girl_r.x -= angle; + while(girl_r.x> 180) girl_r.x-=360; + while(girl_r.x<-180) girl_r.x+=360; + } + } + } + + // anim loops + if( jump_delta < 1 ) { // jump/kick anim +#if 0 + girl.curframe = clampf(girl.curframe, 184, 202); + if( girl.curframe > 202-4 && GAME_FIRE_DOWN ) girl.curframe = 184+4; +#else + #define loopf(frame, min, max) (frame < min ? min : frame > max ? min + frame - max : frame) + if(girl.curframe >= 203) + girl.curframe = loopf(girl.curframe, 203, 220); + else + girl.curframe = clampf(girl.curframe, 184, 202); + if( girl.curframe > 202-4 && girl.curframe < 208 && GAME_FIRE_DOWN ) girl.curframe = 203; +#endif + } + else if( punch_delta < 1 ) { // punch anim + girl.curframe = clampf(girl.curframe, 90, 101); + if( girl.curframe > 101-6 && GAME_FIRE_DOWN ) girl.curframe = 101-6; + } + else if( running ) { + // loop running anim + if( girl.curframe < 65 ) girl.curframe = 65; + if( girl.curframe > 85 ) girl.curframe = 65; + } + else { // loop idle anim + if( girl.curframe > 59 ) girl.curframe = 0; + } + } + + profile("Game.collisions") { + bool punching = girl.curframe >= 90 && girl.curframe < 101; + bool air_kicking = girl.curframe >= 184 && girl.curframe < 202; + bool jump_kicking = girl.curframe >= 203 && girl.curframe < 220; + bool attacking = punching || air_kicking || jump_kicking; + + if( attacking ) { + aabb boxg = model_aabb(girl, girl_pivot); + aabb boxw = model_aabb(witch, witch_pivot); +#if 0 // halve aabb. ok + { + vec3 diag = sub3(boxg.max, boxg.min); + vec3 halve = scale3(diag, 0.25); + vec3 center = scale3(add3(boxg.min, boxg.max), 0.5); + boxg.min = sub3(center, halve); + boxg.max = add3(center, halve); + } +#endif + hit* h = aabb_hit_aabb(boxg, boxw); + if( h && GAME_FIRE ) { + vec3 dir = norm3(sub3(witch_p, girl_p)); + witch_p = add3(witch_p, mul3(dir,vec3(1,0,1))); + } + + if( editor_enabled && editor_ddraw ) { + ddraw_color_push(h ? RED : GREEN); + ddraw_aabb(boxw.min, boxw.max); + ddraw_aabb(boxg.min, boxg.max); + ddraw_color_pop(); + } + } + } + + camera_t *cam = camera_get_active(); + + profile("Game.Draw scene") { + // draw grid/axis + editor_obj_call0(&ground_size, fn_draw); + + // tick+draw girl + editor_obj_call0(&girl, fn_tick); + editor_obj_call0(&girl, fn_draw); + + // tick+draw witch + editor_obj_call0(&witch, fn_tick); + editor_obj_call0(&witch, fn_draw); + } + + if(!editor_enabled) continue; + + profile("Editor.Draw outline") { + + // handle (multi-)selection + ray *r = editor_pickup(); + if( r ) { + bool found = false; + bool multi_selection = input(KEY_LCTRL) || input(KEY_RCTRL); + for each_map_ptr(editor_state, void*, o, editor_state_t, ed) { + void *obj = *o; + if( obj == &ground_size ) continue; // @fixme: add ray+plane. also, bvh + + aabb *box = editor_obj_call0(obj, fn_aabb); + if( ray_hit_aabb(*r, *box)) { + editor_select(obj, multi_selection); + found = true; + } + } + if( !found ) + if( ray_hit_plane(*r, plane(vec3(0,0,0), vec3(0,1,0)) )) { + editor_select(&ground_size, multi_selection); + } + } + + if(!set_count(editor_selection)) continue; + + // draw silhouettes + fx_begin(); + for each_set_ptr(editor_selection, void*, o) { + void *obj = *o; + + editor_obj_call0(obj, fn_draw); + } + fx_end(); + + // draw gizmos, aabbs, markers, etc + for each_set_ptr(editor_selection, void*, o) { + void *obj = *o; + + // get transform + vec3 *p = editor_obj_get_property_by_name(obj, "position"); + vec3 *r = p ? editor_obj_get_property_by_name(obj, "rotation") : NULL; + vec3 *s = r ? editor_obj_get_property_by_name(obj, "scale") : NULL; + + // debugdraw + ddraw_ontop_push(0); + + // bounding box + aabb *box = editor_obj_call0(obj, fn_aabb); + if( box ) { + ddraw_color_push(YELLOW); + ddraw_aabb(box->min, box->max); + ddraw_color_pop(); + } + + // skeleton anim + // model_render_skeleton(model, pivot); + + // position marker + if( p ) { + static map(void*, vec3) prev_dir = 0; + do_once map_init_ptr(prev_dir); + vec3* dir = map_find_or_add(prev_dir, obj, vec3(1,0,0)); + + static map(void*, vec3) prev_pos = 0; + do_once map_init_ptr(prev_pos); + vec3* found = map_find_or_add(prev_pos, obj, *p), fwd = sub3(*p, *found); + if( (fwd.y = 0, len3sq(fwd)) ) { + *found = *p; + *dir = norm3(fwd); + } + + // float diameter = len2( sub2(vec2(box->max.x,box->max.z), vec2(box->min.x,box->min.z) )); + // float radius = diameter * 0.5; + ddraw_position_dir(*p, *dir, 1); + } + + ddraw_ontop(1); + + // transform gizmo + if( p && r && s ) { + gizmo(p,r,s); + } + + ddraw_ontop_pop(); + } + } + } +} + +// @todo +// editor_add_tick_before() +// editor_add_tick_after() +// editor_add_draw_before() +// editor_add_draw_after() diff --git a/tools/editor/editor.png b/tools/editor/editor.png new file mode 100644 index 0000000..d773b05 Binary files /dev/null and b/tools/editor/editor.png differ diff --git a/tools/editor/editor2.c b/tools/editor/editor2.c new file mode 100644 index 0000000..bb96238 --- /dev/null +++ b/tools/editor/editor2.c @@ -0,0 +1,471 @@ +/* + + + + + + +- [ ] Editor: Gizmos✱, scene tree, property editor✱, load/save✱, undo/redo✱, copy/paste, on/off (vis,tick,ddraw,log), vcs. +- [ ] Editor: Scenenode pass: node singleton display, node console, node labels, node outlines✱. +- [ ] Editor: Debug pass: toggles on/off (billboards✱, materials, un/lit, cast shadows, wireframe, skybox✱/mie✱, fog/atmosphere, collide✱, physics). +- [ ] Editor: Level pass: volumes, triggers, platforms, level streaming. +- [ ] Editor: Edit pass: Procedural content, brushes, noise and CSG. +- [ ] Editor: GUI pass: timeline and data tracks, node graphs. +*/ + +#include "fwk.c" +#include "editor2.h" // old editor interface + +#define ui_push_hspace(px) \ + (int xx = px; xx; xx = 0) \ + for(struct nk_panel *layout = ui_ctx->current->layout; layout; ) \ + for( xx = (layout->at_x += px, layout->bounds.w -= px, 0); layout; layout->at_x -= px, layout->bounds.w += px, layout = 0 ) + +// ---------------------------------------------------------------------------------------- + +#define expr expr2 // 3rd_lua.h +#include "3rd_eval.h" + +// ---------------------------------------------------------------------------------------- + +#include "labs.meta/meta_reflect.c" + +int *meta_changed(void *value) { + static map(void*,int) changes = 0; + do_once map_init_ptr(changes); + + return map_find_or_add(changes, value, 0); +} + +void reflect_ui( const reflect *r, void *value, void *userdata ) { + ui_label_icon_highlight = *meta_changed(value); // @hack: remove ui_label_icon_highlight hack + char *title = va(ICON_MD_UNDO "%s", r->info); + + int changed = 0; + /**/ if( !strcmp(r->type, "int") ) changed = ui_int(title, (int*)value); + else if( !strcmp(r->type, "char") && r->is_ptr ) changed = ui_buffer(title, (char*)value, strlen((char*)value)+1); + else if( !strcmp(r->type, "string") ) changed = ui_string(title, (char**)value); + else if( !strcmp(r->type, "float") ) changed = ui_float(title, (float*)value); + else if( !strcmp(r->type, "double") ) changed = ui_double(title, (double*)value); + else if( !strcmp(r->type, "unsigned") ) changed = ui_unsigned(title, (unsigned*)value); + else if( !strcmp(r->type, "color") ) changed = ui_color4(va("%s #%02X%02X%02X%02X", title, (int)(0[(float*)value]),(int)(1[(float*)value]),(int)(2[(float*)value]),(int)(3[(float*)value])), (float*)value); + // else if( !strcmp(type, "vec3") ) ; // not supported. decays to 3 floats + else ui_label2(title, va("(%s)%s", r->type, r->name)); + + if( changed ) { + *meta_changed(value) = 1; + } + + if( ui_label_icon_clicked_L.x >= 6 && ui_label_icon_clicked_L.x <= 26 ) { // @hack: if clicked on UNDO icon (1st icon) + *meta_changed(value) = 0; + } +} +bool reflect_parse(void *obj, const char *type, const char *val) { + /**/ if( !strcmp(type, "int") ) *((int*)obj) = eval(val); +// else if( !strcmp(r->type, "char") && r->is_ptr ) ; // @fixme: not supported, unless we do strncpy() or similar. + else if( !strcmp(type, "string") ) *((char**)obj) = stringf("%s", val); + else if( !strcmp(type, "float") ) *((float*)obj) = eval(val); // = v[0] == '~' ? (float)~atoi(val+1) : atof(val); // = atof(val); + else if( !strcmp(type, "double") ) *((double*)obj) = eval(val); // = v[0] == '~' ? (float)~atoi(val+1) : atof(val); // = atof(val); + else if( !strcmp(type, "unsigned") ) *((unsigned*)obj) = eval(val); // = v[0] == '~' ? (float)~atoi(val+1) : atof(val); // = atof(val); + else if( !strcmp(type, "color") ) *(((float*)obj)+3) = 255, sscanf(val, "%f %f %f %f", ((float*)obj)+0, ((float*)obj)+1, ((float*)obj)+2, ((float*)obj)+3); + else if( !strcmp(type, "vec3") ) sscanf(val, "%f %f %f", ((float*)obj)+0, ((float*)obj)+1, ((float*)obj)+2); + else return 0; + return 1; +} + +// ---------------------------------------------------------------------------------------- + +typedef void(*obj_ctor)(void*); +static map(char*, obj_ctor) obj_ctors; + +#define STRUCT_CTOR(type, ctor) STRUCT_CTOR(#type, (obj_ctor)ctor) +void (STRUCT_CTOR)( const char *type, obj_ctor ctor ) { + do_once map_init_str(obj_ctors); + map_find_or_add(obj_ctors, STRDUP(type), ctor); +} + +bool obj_make(void *obj, const char *ini_data) { // initialize object from ini datas + char *hint = 0; + + for( ini_t read = ini_from_mem(ini_data); !!read; map_free(read), read = 0) { + for each_map(read, char*, k, char*, v) { + array(char*) tokens = strsplit(k, "."); + if( array_count(tokens) != 2 ) continue; + + const char *type = 0; + void *found = reflect_field( tokens[0], obj, tokens[1], &type ); + if( !found ) continue; + + if( reflect_parse(found, type, v) ) { + hint = tokens[0]; + } + } + + // constructor (post-init call) + obj_ctor *ctor = map_find(obj_ctors, hint); + if( ctor ) (*ctor)( obj ); + } + + return hint != 0; +} + +// ---------------------------------------------------------------------------- + +#define POD_TYPES \ + vec2i v2i; \ + vec2 v2; \ + vec3 v3; \ + vec4 v4; \ + quat q; \ + char *s; \ + double d; \ + int64_t i; \ + unsigned color; \ + void *ptr; \ + void (*fun)(void*); \ + char *nametype; /* "name\0type" */ + +typedef union pod { + POD_TYPES +} pod; + +typedef union var { + POD_TYPES + array(pod) array; + array(pod) pair; // array of 2: first,second. maybe used as key,value + array(pod) tuple; // array of N: first,second,third... maybe used as array of N*2: NAME1,val1,NAME2,val2, ... + map(pod,pod) map; + set(pod) set; +} var; + +typedef struct node { + var k; + var v; + + struct node *up; + struct node *next; + struct node *down; +} node; + +char* node_name(node *n) { + return n->k.nametype; +} +char* node_type(node *n) { + return n->k.nametype + strlen(n->k.nametype) + 1; +} +char* node_set_nametype(node *n, const char *name, const char *type) { + *strchr(n->k.nametype = stringf("%s\1%s", name, type), '\1') = '\0'; // @leak + return n->k.nametype; +} +node* node_find(node *n, const char *path) { + if (!n->k.nametype) return 0; + if( !strcmp(node_name(n), path) ) return n; + node *r = 0; + if( n->next ) r = node_find(n->next, path); + if( n->down && !r ) if( !strcmp(node_name(n->down), path) ) return n->down; // r = node_find(n->down, path); + return r; +} +node* node_find_recurse(node *n, const char *path) { + array(char*) split = strsplit(path, "/"); + while( n && array_count(split) ) { + n = node_find(n, split[0]); + array_pop(split); + } + return n; +} +node* node_attach_sibling(node *n, node *sibling) { + while( n->next ) n = n->next; + return n->next = sibling; +} +node* node_attach_child(node *n, node *child) { + child->up = n; + if( n->down ) return node_attach_sibling(n->down, child); + return n->down = child; +} +unsigned node_children(node *n) { + unsigned c = 0; + if( n->down ) { + n = n->down; + do ++c; while( (n = n->next) ); + } + return c; +} +unsigned node_siblings(node *n) { + return n->up ? node_children( n->up->down ) : 0; +} +void node_init(node *n) { + profile_incstat("Editor.inits", +1); + + if(n->next) node_init(n->next); + if(n->down) node_init(n->down); +} +void node_tick(node *n) { + profile_incstat("Editor.ticks", +1); + + if(n->next) node_tick(n->next); + if(n->down) node_tick(n->down); +} +void node_draw(node *n) { + profile_incstat("Editor.draws", +1); + + if(n->next) node_draw(n->next); + if(n->down) node_draw(n->down); +} +void node_quit(node *n) { + profile_incstat("Editor.quits", +1); + + if(n->next) node_quit(n->next); + if(n->down) node_quit(n->down); +} +void node_edit(node *n, node *root) { + profile_incstat("Editor.edits", +1); + + if( ui_collapse(va("%s %s (%u)", n->down ? ICON_MD_SOURCE : ICON_MD_FOLDER, node_name(n), node_children(n)), va("%p%p",root,n->v.ptr)) ) { // @fixme v.ptr + if( n->down ) node_edit(n->down,root); + + if( reflect_has_fields( node_type(n), n->v.ptr ) ) { + for ui_push_hspace( 4 ) { + #define ICON_DOT ICON_CANCEL // ICON_MD_WIFI_1_BAR // ICON_MD_RADIO_BUTTON_UNCHECKED // ICON_MD_LENS_BLUR + static int flags[4] = {0}; + char *toolbar = va("%s%s%s%s", + flags[3] ? ICON_MD_STAR : ICON_MD_STAR_OUTLINE, // ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER, // flags[3] == 0 ? ICON_MD_STAR_OUTLINE : flags[3] == 1 ? ICON_MD_STAR_HALF : ICON_MD_STAR, + flags[2] ? ICON_MD_CODE : ICON_DOT, + flags[1] ? ICON_MD_FLAG : ICON_DOT, + flags[0] ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF + ); + + ui_label_icon_highlight = *meta_changed(n); // @hack: remove ui_label_icon_highlight hack + char *section = va("*" ICON_MD_UNDO "%s", node_type(n)); + + int choice = ui_label2_toolbar(section, toolbar); + if( choice ) flags[ choice - 1 ] = (flags[ choice - 1 ] + 1 ) % ( choice == 4 ? 2/*3*/ : 2); + reflect_iterate_fields( node_type(n), n->v.ptr, reflect_ui, NULL ); // @fixme v.ptr + } + } + + ui_collapse_end(); + } + + if(n->next) node_edit(n->next,root); +} + +// --- + +struct editor_t { + unsigned frame; + // root nodes + node init; + node tick; + node draw; + node edit; + node quit; +} editor; + +enum { EDITOR_BUCKETS = 5 }; // init+tick+draw+edit+quit + +void editor_reset() { + node_quit(&editor.quit); + editor.frame = 0; +} +void editor_frame() { + editor_init(); // old editor interface + editor_tick(); // old editor interface + editor_menubar(); // old editor interface + + if( input_down(KEY_F5) ) { + editor_reset(); + } + if( editor.frame++ == 0 ) { + node_init(&editor.init); + } + node_tick(&editor.tick); + node_draw(&editor.draw); + + // content browser + if( ui_window("File Browser", 0) ) { + const char *file = 0; + if( ui_browse(&file, NULL) ) { + const char *sep = ifdef(win32, "\"", "'"); + app_exec(va("%s %s%s%s", ifdef(win32, "start \"\"", ifdef(osx, "open", "xdg-open")), sep, file, sep)); + } + ui_window_end(); + } + + // console/terminal window + if( 0 && ui_window("Console", 0) ) { // @fixme half-working + ui_console(); + ui_window_end(); + } + + if( ui_window("Outliner", 0) ) { + +#if 1 + static char *filter = 0; + { + static int do_filter = 0; + int choice = ui_toolbar(ICON_MD_SEARCH ";" ICON_MD_REFRESH ";" ICON_MD_SD_CARD); + if( choice == 1 ) do_filter = 1; + if( do_filter ) { + ui_string(ICON_CANCEL " Filter " ICON_MD_SEARCH, &filter); + if( ui_label_icon_clicked_L.x > 0 && ui_label_icon_clicked_L.x <= 24 ) { // if clicked on CANCEL icon (1st icon) + do_filter = 0; + } + } else { + if( filter ) filter[0] = '\0'; + } + char *filter_mask = filter && filter[0] ? va("*%s*", filter) : "*"; + } +#endif + + for( int c = ui_collapse(ICON_MD_FOLDER_SPECIAL " Art/", "ART"); c; ui_collapse_end(), c = 0) { + static const char *file; + static bool show_browser = 1; + if( ui_browse(&file, &show_browser) ) { + app_exec(va("%s %s", ifdef(win32, "start", ifdef(osx, "open", "xdg-open")), file)); + //puts(file); + show_browser = 1; + } + } + for( int c = ui_collapse(ICON_MD_BOOKMARK " Bookmarks/", "BOOK"); c; ui_collapse_end(), c = 0) { + } + for( int c = ui_collapse(ICON_MD_BUBBLE_CHART/*ICON_MD_SCATTER_PLOT*/ " Entities/", "ENT"); c; ui_collapse_end(), c = 0) { + } + for( int c = ui_collapse(ICON_MD_TUNE " Components/", "COM"); c; ui_collapse_end(), c = 0) { + } + for( int c = ui_collapse(ICON_MD_PRECISION_MANUFACTURING " Systems/", "SYS"); c; ui_collapse_end(), c = 0) { + } + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Hierarchy/", "ORD"); c; ui_collapse_end(), c = 0) { + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Init/", "ORD1"); c; ui_collapse_end(), c = 0) {} + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Draw/", "ORD2"); c; ui_collapse_end(), c = 0) {} + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Tick/", "ORD3"); c; ui_collapse_end(), c = 0) {} + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Edit/", "ORD4"); c; ui_collapse_end(), c = 0) {} + for( int c = ui_collapse(ICON_MD_ACCOUNT_TREE " Quit/", "ORD5"); c; ui_collapse_end(), c = 0) {} + + // node_edit(&editor.init,&editor.init); + // node_edit(&editor.draw,&editor.draw); + // node_edit(&editor.tick,&editor.tick); + // node_edit(&editor.edit,&editor.edit); + // node_edit(&editor.quit,&editor.quit); + } + for( int c = ui_collapse(ICON_MD_PUBLIC " World/", "WORLD"); c; ui_collapse_end(), c = 0) { + node_edit(editor.edit.down,&editor.edit); + } + + ui_window_end(); + } +} + +unsigned editor_spawn(const char *path_id, const char *keytype, void *val) { + do_once { + node_set_nametype(&editor.init, "Init Group", "init_nodes"); // @leak + node_set_nametype(&editor.tick, "Tick Group", "tick_nodes"); // @leak + node_set_nametype(&editor.draw, "Draw Group", "draw_nodes"); // @leak + node_set_nametype(&editor.edit, "Edit Group", "edit_nodes"); // @leak + node_set_nametype(&editor.quit, "Quit Group", "quit_nodes"); // @leak + } + + array(char*) tokens = strsplit(path_id, "/"); + char *keyname = *array_back(tokens); + + array(node) n = 0; + array_resize(n, EDITOR_BUCKETS); + for( int i = 0; i < EDITOR_BUCKETS; ++i ) { + node_set_nametype(&n[i], keyname, keytype); // @leak + n[i].v.ptr = val; + } + + array_pop(tokens); + char *joint = array_count(tokens) ? strjoin(tokens, "/") : "/"; + + node *p = 0; + p = node_find_recurse(&editor.init, joint), node_attach_child(p ? p : &editor.init, n+0); + p = node_find_recurse(&editor.tick, joint), node_attach_child(p ? p : &editor.tick, n+1); + p = node_find_recurse(&editor.draw, joint), node_attach_child(p ? p : &editor.draw, n+2); + p = node_find_recurse(&editor.edit, joint), node_attach_child(p ? p : &editor.edit, n+3); + p = node_find_recurse(&editor.quit, joint), node_attach_child(p ? p : &editor.quit, n+4); + return 0; +} + +// demo ----------------------------------------------------------------------- + +typedef struct my_sprite { + char *filename; + vec3 position; + float tilt; + vec4 tint; + // --- private + texture_t texture_; + unsigned bgra_; +} my_sprite; + +void my_sprite_ctor(my_sprite *obj) { + obj->texture_ = texture(obj->filename, TEXTURE_RGBA); + obj->bgra_ = bgraf( obj->tint.r/255.0, obj->tint.g/255.0, obj->tint.b/255.0, obj->tint.a/255.0 ); +} +void my_sprite_draw(my_sprite *obj) { + obj->bgra_ = bgraf( obj->tint.r/255.0, obj->tint.g/255.0, obj->tint.b/255.0, obj->tint.a/255.0 ); // @fixme: del me + sprite( obj->texture_, &(obj->position.x), obj->tilt, obj->bgra_ ); +} + +int main() { + typedef char* string; + typedef vec4 color; + + STRUCT( vec3, float, x, "X" ); + STRUCT( vec3, float, y, "Y" ); + STRUCT( vec3, float, z, "Z" ); + +// STRUCT( texture_t, unsigned, flags, "Flags"); +// STRUCT( texture_t, string, filename, "Filename"); + + STRUCT( my_sprite, string, filename, "Filename" ); + STRUCT( my_sprite, vec3, position, "Position" ); + STRUCT( my_sprite, float, tilt, "Tilt degrees" ); + STRUCT( my_sprite, color, tint, "Tint color" ); + STRUCT_CTOR( my_sprite, my_sprite_ctor ); + + PRINTF("pod:%d, var:%d, node:%d warn\n", (int)sizeof(pod), (int)sizeof(var), (int)sizeof(node)); + PRINTF("reflected:%d bytes vs real:%d bytes warn\n", reflect_sizeof("my_sprite"), (int)sizeof(my_sprite)); + + // cook_config("../../tools/cook.ini"); + window_create(0.80, 0); + + struct my_sprite spr1 = {0}, spr2 = {0}, spr3 = {0}; + obj_make(&spr1, + "[my_sprite]\n" + "filename=cat.png\n" + "position=5 2 100\n" + "tilt=45 + 45 -90\n" + "tint=255 255 0\n" + ); + obj_make(&spr2, + "[my_sprite]\n" + "filename=cat.png\n" + "position=1 2 100\n" + "tilt=45 + 45 -90\n" + "tint=255 0 0\n" + ); + obj_make(&spr3, + "[my_sprite]\n" + "filename=cat.png\n" + "position=1 2 100\n" + "tilt=45\n" + "tint=0 0 255\n" + ); + int hero1 = editor_spawn("/hero1", "my_sprite", &spr1); + int hero2 = editor_spawn("/hero2", "my_sprite", &spr2); + int hero3 = editor_spawn("/hero1/heroB", "my_sprite", &spr3); + + camera_t cam = camera(); + camera_enable(&cam); + + while( window_swap() ) { + editor_frame(); + + // @fixme: this should be drawn by game, not editor! + my_sprite_draw(&spr1); + my_sprite_draw(&spr2); + my_sprite_draw(&spr3); + // spr1.tilt = 5 * sin(time_ss()); + } +} diff --git a/tools/editor/editor2.h b/tools/editor/editor2.h new file mode 100644 index 0000000..e82ab0c --- /dev/null +++ b/tools/editor/editor2.h @@ -0,0 +1,343 @@ +#define EDITOR_VERSION "2022.7" + +#if 1 +#define EDITOR_PRINTF PRINTF +#else +#define EDITOR_PRINTF(...) do {} while(0) +#endif + +#define ICON_PLAY ICON_MD_PLAY_ARROW +#define ICON_PAUSE ICON_MD_PAUSE +#define ICON_STOP ICON_MD_STOP +#define ICON_CANCEL ICON_MD_CLOSE + +#define ICON_WARNING ICON_MD_WARNING +#define ICON_BROWSER ICON_MD_FOLDER_SPECIAL +#define ICON_OUTLINER ICON_MD_VIEW_IN_AR +#define ICON_BUILD ICON_MD_BUILD +#define ICON_SCREENSHOT ICON_MD_PHOTO_CAMERA +#define ICON_CAMERA_ON ICON_MD_VIDEOCAM +#define ICON_CAMERA_OFF ICON_MD_VIDEOCAM_OFF +#define ICON_GAMEPAD_ON ICON_MD_VIDEOGAME_ASSET +#define ICON_GAMEPAD_OFF ICON_MD_VIDEOGAME_ASSET_OFF +#define ICON_AUDIO_ON ICON_MD_VOLUME_UP +#define ICON_AUDIO_OFF ICON_MD_VOLUME_OFF +#define ICON_WINDOWED ICON_MD_FULLSCREEN_EXIT +#define ICON_FULLSCREEN ICON_MD_FULLSCREEN +#define ICON_LIGHTS_ON ICON_MD_LIGHTBULB +#define ICON_LIGHTS_OFF ICON_MD_LIGHTBULB_OUTLINE +#define ICON_RENDER_BASIC ICON_MD_IMAGE_SEARCH +#define ICON_RENDER_FULL ICON_MD_INSERT_PHOTO + +#define ICON_SIGNAL ICON_MD_SIGNAL_CELLULAR_ALT +#define ICON_DISK ICON_MD_STORAGE +#define ICON_RATE ICON_MD_SPEED + +#define ICON_CLOCK ICON_MD_TODAY +#define ICON_CHRONO ICON_MD_TIMELAPSE + +#define ICON_SETTINGS ICON_MD_SETTINGS +#define ICON_LANGUAGE ICON_MD_G_TRANSLATE +#define ICON_PERSONA ICON_MD_FACE +#define ICON_SOCIAL ICON_MD_MESSAGE +#define ICON_GAME ICON_MD_ROCKET_LAUNCH +#define ICON_KEYBOARD ICON_MD_KEYBOARD +#define ICON_MOUSE ICON_MD_MOUSE +#define ICON_GAMEPAD ICON_MD_GAMEPAD +#define ICON_MONITOR ICON_MD_MONITOR +#define ICON_WIFI ICON_MD_WIFI +#define ICON_BUDGET ICON_MD_SAVINGS +#define ICON_NEW_FOLDER ICON_MD_CREATE_NEW_FOLDER +#define ICON_PLUGIN ICON_MD_EXTENSION +#define ICON_RESTART ICON_MD_REPLAY +#define ICON_QUIT ICON_MD_CLOSE + +#define ICON_POWER ICON_MD_BOLT // ICON_MD_POWER +#define ICON_BATTERY_CHARGING ICON_MD_BATTERY_CHARGING_FULL +#define ICON_BATTERY_LEVELS \ + ICON_MD_BATTERY_ALERT, \ + ICON_MD_BATTERY_0_BAR,ICON_MD_BATTERY_1_BAR, \ + ICON_MD_BATTERY_2_BAR,ICON_MD_BATTERY_3_BAR, \ + ICON_MD_BATTERY_4_BAR,ICON_MD_BATTERY_5_BAR, \ + ICON_MD_BATTERY_6_BAR,ICON_MD_BATTERY_FULL + +// state - retained mode + +typedef int property; // @fixme + +typedef struct editor_state_t { + array(property) properties; + array(char) buffer; + array(vec2i) history; + unsigned cursor; +} editor_state_t; + +typedef map(char*, char*) editor_dict_t; + +static map(void*, editor_state_t) editor_state; // world +static map(void*, editor_dict_t) editor_dicts; +static set(void*) editor_world; +static set(void*) editor_selection; // objects selected in scene + +// editor controls + +//static int editor_attached = 1; +static int editor_enabled = 1; +static void* editor_selected_obj = 0; +static int editor_key = 0; +static vec2 editor_mouse = {0}; // 2d coord for ray/picking +static bool editor_gamepad = 1; +static int editor_hz = 60; +static int editor_hz_mid = 18; +static int editor_hz_low = 5; +static bool editor_power_saving = 0; +static double editor_t = 0, editor_dt = 0; +static bool editor_lit = 1; +static bool editor_ddraw = 1; + +static +void editor_init() { + do_once { + map_init_ptr(editor_state); + map_init_ptr(editor_dicts); + set_init_ptr(editor_world); + set_init_ptr(editor_selection); + profile_enable( false ); + window_pause( true ); + } +} + +void editor_tick() { + // timing + editor_dt = window_delta() * !window_has_pause(); if(editor_dt > 1/60.f) editor_dt = 1/60.f; +} + +bool editor_active() { + return ui_hover() || ui_active() || gizmo_active() ? editor_enabled : 0; +} +double editor_ss() { + return 1000 + editor_t; +} +double editor_delta() { + return editor_dt; +} + +void editor_select_all() {} +void editor_select_none() {} + + +enum editor_keys { + key_none, + key_pause, + key_reload, + key_browser, + key_recording, + key_fullscreen, + key_screenshot, // @todo: add meta-info in exif or invisibile pixels (cam details, player details, map level, map location, level state, etc) + key_quit, + key_mute, + key_battery, + key_profiler, + key_stop, + key_outliner, + key_undo, + key_redo, + key_save_mem, + key_save_disk, + key_load_disk, + key_reset, + key_debugger, + key_gamepad, + key_lit, + key_ddraw, +}; + +void editor_menubar() { + do_once editor_init(); + + int alts = input(KEY_LALT) || input(KEY_RALT); // @todo: move to fwk.c + int ctrls = input(KEY_LCTRL) || input(KEY_RCTRL); // @todo: move to fwk.c + int shifts = input(KEY_LSHIFT) || input(KEY_RSHIFT); // @todo: move to fwk.c + int mods = alts || ctrls || shifts; // @todo: move to fwk.c + if( input_down(KEY_F5) ) editor_key = key_reload; + if( input_down(KEY_F11) ) editor_key = key_fullscreen; + if( input_down(KEY_PAUSE) ) editor_key = key_pause; + if( input_down(KEY_PRINT) ) editor_key = (mods ? key_recording : key_screenshot); + // if( input_down(KEY_W) && input_held(KEY_LCTRL) ) editor_key = key_quit; + + if( ctrls ) { + /**/ if( input_down(KEY_Z) ) editor_key = key_undo; + else if( input_down(KEY_Y) ) editor_key = key_redo; + else if( input_down(KEY_S) ) editor_key = key_save_disk; + else if( input_down(KEY_A) ) editor_select_all(); + else if( input_down(KEY_D) ) editor_select_none(); + } + + if( !editor_key && editor_selected_obj ) { + if( input_up(MOUSE_L) ) editor_key = key_save_mem; + if( input_down(MOUSE_R) ) ui_show("Properties", true); + } + + // @fixme: send all editor keys to game? + // if( input_repeat(KEY_ESC, 300)) {} + // if( input_repeat(KEY_F1, 300)) {} + // etc... + + // menubar + + if( ui_menu( ICON_SETTINGS "@Preferences;" + ICON_LANGUAGE " Language;" + ICON_PERSONA " Profile;" // editor account, but also fake profile and 1st party credentials + ICON_SOCIAL " Social;" + ICON_GAME " Game;" // + ICON_KEYBOARD " Keyboard;" + ICON_MOUSE " Mouse;" + ICON_GAMEPAD " Gamepad;" + ICON_MONITOR " Display;" // @todo: RENDER settings, AUDIO settings + ICON_WIFI " Network;" + ICON_BUDGET " Budget;" // mem,gfx,net,hdd,... also logging + ICON_NEW_FOLDER " Folders;" // including project folders + ICON_PLUGIN " Plugins;" // including VCS + ICON_RESTART " Restart;" + ICON_QUIT " Quit;" + "-" ICON_MD_RECYCLING " Reset all preferences;" ICON_MD_SAVE_AS " Save all preferences" + ) ) { + if( ui_item() == 3 ) {} // key mappings + if( ui_item() == 4 ) {} // sensitivity, invert xylrw + if( ui_item() == 5 ) {} // sensitivity, invert xy,ab, deadzones + if( ui_item() == 7 ) {} // name,email,icon,link,github + if( ui_item() == 13) editor_key = key_reload; + if( ui_item() == 14) editor_key = key_quit; + } + + static char game_args[16] = "--game-args"; // @fixme @todo remove '_' special char to signal that ui_menu() is writeable (inputbox) + if( ui_menu_editbox( game_args, 16 ) ) {} + + if( ui_menu( ICON_CANCEL "@Cancel" ) ) {} + + if( ui_menu( window_has_pause() ? ICON_PLAY "@Tap to Play Game" : ICON_PAUSE "@Tap to Pause Game" )) editor_key = key_pause; + if( ui_menu( ICON_MD_SKIP_NEXT "@Next frame") ) {} + if( ui_menu( ICON_MD_FAST_FORWARD "@Fast forward") ) {} + //if( ui_menu( ICON_STOP "@Stop game" )) editor_key = key_stop; + + if( ui_menu( va(ICON_BUILD "@Build game"))) ui_notify("Build", ICON_WARNING " Not implemented."); + if( ui_menu( va(ICON_MD_ROCKET_LAUNCH "@Launch game"))) ui_notify("Launch", ICON_WARNING " Not implemented."); + + // ICON_MD_TROUBLESHOOT -> PROFILER + // ICON_MD_SCHEMA -> GRAPHNODES + // ICON_MD_ACCOUNT_TREE -> GRAPHNODES + // ICON_MD_TIPS_AND_UPDATES -> BULB + // if( ui_menu( ICON_MD_MENU )) {} + +// if( ui_menu( ICON_BROWSER "@Content browser" )) editor_key = key_browser; +// if( ui_menu( va(ICON_OUTLINER " %d/%d@World outliner", set_count(editor_selection), map_count(editor_state) ))) editor_key = key_outliner; + + if( ui_menu( ICON_SCREENSHOT "@Take Screenshot" )) editor_key = key_screenshot; // MD_SCREENSHOT_MONITOR + if( ui_menu( record_active() ? ICON_CAMERA_OFF "@Stop video recording" : ICON_CAMERA_ON "@Start video recording" )) { if(record_active()) record_stop(); else editor_key = key_recording; } + if( ui_menu( editor_gamepad ? ICON_GAMEPAD_ON "@Gamepad is enabled. Tap to disable." : ICON_GAMEPAD_OFF "@Gamepad is disabled. Tap to enable." )) editor_key = key_gamepad; + if( ui_menu( audio_volume_master(-1) > 0 ? ICON_AUDIO_ON "@Audio is enabled. Tap to mute." : ICON_AUDIO_OFF "@Audio is muted. Tap to enable." )) editor_key = key_mute; + if( ui_menu( window_has_fullscreen() ? ICON_WINDOWED "@Fullscreen. Tap to go Windowed." : ICON_FULLSCREEN "@Windowed. Tap to go Fullscreen." )) editor_key = key_fullscreen; + + if( ui_menu( editor_ddraw ? ICON_RENDER_BASIC "@Debug renderer. Tap to go Retail Renderer." : ICON_RENDER_FULL "@Retail renderer. Tap to go Debug Renderer." )) editor_key = key_ddraw; // ICON_MD_ADD_PHOTO_ALTERNATE + if( ui_menu( editor_lit ? ICON_LIGHTS_ON "@Lit. Tap to disable lighting." : ICON_LIGHTS_OFF "@Unlit. Tap to enable lighting." )) editor_key = key_lit; + + // logic: either plug icon (power saving off) or one of the following ones (power saving on): + // if 0% batt (no batt): battery alert + // if discharging: battery levels [alert,0..6,full] + // if charging: battery charging + int battery_read = app_battery(); + int battery_level = abs(battery_read); + int battery_discharging = battery_read < 0 && battery_level < 100; + const char *battery_levels[] = { // @todo: remap [7%..100%] -> [0..1] ? + ICON_BATTERY_LEVELS + }; + if( ui_menu( !editor_power_saving ? ICON_POWER"@Full power. Tap to save power." : + va("%s@Power saving. Tap to go full power. %3d%% battery.", + battery_read == 0 ? battery_levels[0] : + battery_discharging ? battery_levels[(int)((countof(battery_levels)-1)*clampf(battery_level/100.f,0,1))] : + ICON_BATTERY_CHARGING, battery_level) )) + editor_key = key_battery; + + // @todo: combine-in-1? cycle mem -> cpu/profiler -> network mon -> debugger + + // bug report, signal status, disk status, framerate status + if( ui_menu(va(ICON_SIGNAL " 0/0KiB" ))) {} // SIGNAL_CELLULAR_1_BAR SIGNAL_CELLULAR_2_BAR + if( ui_menu(va(ICON_DISK " %s", xstats() ))) {} // 012/136MB + if( ui_menu(va(ICON_RATE " %5.2f/%d", window_fps(), (int)window_fps_target()))) editor_key = key_profiler; // 012/136MB + + // bug report, profile, warnings, time/chrono (@todo: alarm/snooze? calendar?) + if( ui_menu( ICON_MD_BUG_REPORT /*"^"*/ "0" ) ) {} + if( ui_menu( ICON_MD_FACE /*"^"*/ "3" ) ) {} // @todo: do both messaging/warnings + profile settings here + { + static double base = 0, tap = 0; + + if( tap == 0 ) tap = time_ss(); + double delta = time_ss() - tap; + tap = time_ss(); + base += delta * !window_has_pause(); + + if( ui_menu( base == 0 ? + va(ICON_CLOCK "%02d:%02d", (int)((date() / 10000) % 100), (int)((date() / 100) % 100)) + : + va(ICON_CHRONO "%03dm:%02ds:%02df@Tap to reset chrono.",((int)(base/60))%60,((int)base)%60,(int)((base - (int)base)*window_fps_target()))) + || editor_key == key_stop + ) { + base = 0, tap = 0; + } + } + + for each_map_ptr(editor_state, void *, o, editor_state_t, ed) { + profile_incstat("Editor.num_objects", +1); + + void *obj = *o; + +#if 1 +#elif 0 + // auto-load from disk during init. @fixme kvdb database + if( array_count(ed->history) == 0 ) + if( editor_load_disk(obj, editor_obj_string(obj, ".path")) ) + {} + + // auto-save in-mem during first edit + if( array_count(ed->history) == 0 ) + editor_save_mem(obj); +#endif + + // @todo: continue if obj not found in selection set + if( obj != editor_selected_obj ) + continue; + + if( editor_key == key_debugger ) { breakpoint("User requested breakpoint on this object"); } +#if 1 +#elif 0 + if( editor_key == key_reset ) { const char *ok = editor_reset(obj) ? "ok" : "err"; EDITOR_PRINTF("reset: %s\n", ok); } + if( editor_key == key_save_mem ) { const char *ok = editor_save_mem(obj) ? "ok" : "err"; EDITOR_PRINTF("mem saved: %s\n", ok); } + if( editor_key == key_undo ) { const char *ok = editor_undo(obj) ? "ok" : "err"; EDITOR_PRINTF("undo: %s\n", ok); } + if( editor_key == key_redo ) { const char *ok = editor_redo(obj) ? "ok" : "err"; EDITOR_PRINTF("redo: %s\n", ok); } + if( editor_key == key_save_disk ) { const char *ok = editor_save_disk(obj, editor_obj_string(obj, ".path")) ? "ok" : "err"; EDITOR_PRINTF("save: %s\n", ok); } + if( editor_key == key_load_disk ) { const char *ok = editor_load_disk(obj, editor_obj_string(obj, ".path")) ? "ok" : "err"; EDITOR_PRINTF("load: %s\n", ok); } +#endif + } + + char *name; + switch( editor_key ) { + default: + break; case key_quit: record_stop(), exit(0); + break; case key_stop: window_pause(1); + break; case key_mute: audio_volume_master( 1 ^ !!audio_volume_master(-1) ); + break; case key_pause: window_pause( window_has_pause() ^ 1 ); + break; case key_reload: window_reload(); + break; case key_battery: editor_power_saving ^= 1; + break; case key_browser: ui_show("File Browser", ui_visible("File Browser") ^ true); + break; case key_outliner: ui_show("Outliner", ui_visible("Outliner") ^ true); + break; case key_recording: name = file_counter(va("%s.mp4",app_name())), window_record(name), ui_notify(va("Video capturing: %s", name), date_string()); + break; case key_screenshot: name = file_counter(va("%s.png",app_name())), window_screenshot(name), ui_notify(va("Screenshot: %s", name), date_string()); + break; case key_profiler: ui_show("Profiler", profile_enable(ui_visible("Profiler") ^ true)); + break; case key_fullscreen: record_stop(), window_fullscreen( window_has_fullscreen() ^ 1 ); // framebuffer resizing corrupts video stream, so stop any recording beforehand + break; case key_gamepad: editor_gamepad ^= 1; + break; case key_lit: editor_lit ^= 1; + break; case key_ddraw: editor_ddraw ^= 1; + } + + editor_key = 0; +} diff --git a/tools/editor/labs.meta/meta_dna.c b/tools/editor/labs.meta/meta_dna.c new file mode 100644 index 0000000..7db2ba1 --- /dev/null +++ b/tools/editor/labs.meta/meta_dna.c @@ -0,0 +1,328 @@ +// @todo fsave: fputs(DNA), then fwrite +// @todo fread: fgets(DNA), abort if DNA != read; then fread + +#include + +// load/save whole struct acording to its DNA structure +int fload(FILE *infile, void *structure, const char *dna); +int fsave(FILE *outfile, const void *structure, const char *dna); + +// set alignment for next fload/fsave call. resets back to 0 after every fload/fsave call. +// value: 0 (auto, default), 1,2,4,8,16 bytes [...] +void falign(unsigned alignment); + +// override default DNA handlers and switch to a different schema and/or serialization format +typedef int (*size_operator)(char fmt, void *addr, int readmode); +typedef int (*call_operator)(void *out, void *addr, char fmt, int count, int readmode); +void foverride(size_operator size, call_operator call); + +#if 0 +Example Usage +------------- + + struct fat_bootsector { + uint8_t jump_instruction[3]; + uint8_t oem_name[8]; + uint16_t bytes_per_sector; + uint8_t sectors_per_cluster; + uint16_t reserved_sectors; + uint8_t fat_copies; + uint16_t max_dirs; + uint16_t sector_count; + uint8_t media_descriptor; + uint16_t sectors_per_fat; + uint16_t sectors_per_head; + uint16_t heads; + uint32_t hidden_sectors; + uint32_t sector_count2; + } fat_struct; + +Now we can read a binary image of the MBR into this structure: + + mreadf(mbr, "i3c8chchchhchhhdd", &fat_struct); + +Directives +---------- + +Supported directives: + + regex meaning + + (blank) ignored + + b read / write uint8_t + w read / write uint16_t (uppercase: use vli encoding; as uint8? ) + u read / write uint32_t (uppercase: use vli encoding; as uint8,16? ) + q read / write uint64_t (uppercase: use vli encoding; as uint8,16,32?) + + m read / write micro + h read / write half (uppercase: use smallest representation; as micro?) + f read / write float (uppercase: use smallest representation; as micro,half?) + d read / write double (uppercase: use smallest representation; as micro,half,float?) + + s read / write string (uppercase: use smallest representation; quarks?) + [] read / write buffer + + < switch to Intel (little endian) byte order + > switch to Motorola (big endian) byte order + ( begin of tight group + ) end of tight group + [0-9]+ next item repeated n times + z skip one byte of input / emit \0 + * consume next structure item but do not read / write +#endif + +// ---------------------------------------------------------------------------- + +#include +#include +#include + +#ifndef __thread +#define __thread __declspec(thread) +#endif + +static __thread int pragma_pack_alignment = 0; + +void falign(unsigned alignment) { + pragma_pack_alignment = alignment; +} + +static +int size_fn(char fmt, uint8_t* addr8, int rd) { + if(addr8) { + // sizeof pointee data & align operator + /**/ if(fmt == 'c' || fmt == 'b') return 1; + else if(fmt == 'w' ) return 2; + else if(fmt == 'i' || fmt == 'u') return 4; + else if(fmt == 'l' || fmt == 'q') return 8; + else if(fmt == 'f' ) return 4; + else if(fmt == 'd' ) return 8; + else if(fmt == 's' ) return !*(char**)addr8 ? 0+1 : !rd ? strlen(*(char**)addr8) + 1 : strcspn(*(char**)addr8,"\x1") + 1; + return -1; + } else { + // sizeof member + /**/ if(fmt == 'w' ) return 2; + else if(fmt == 'i' || fmt == 'u') return 4; + else if(fmt == 'l' || fmt == 'q') return 8; + else if(fmt == 'f' ) return 4; + else if(fmt == 'd' ) return 8; + else if(fmt == 's' ) return sizeof(void*); + return 1; + } +} + +static +int call_fn(void *out, uint8_t *addr8, char fmt, int count, int rd) { // rd/wr operator + FILE *fp = (FILE*)out; + int iterated_bytes = 0, processed_bytes = 0; + while( --count >= 0 ) { + int slot = size_fn(fmt, 0, rd); + int size = size_fn(fmt, addr8, rd); + if(rd) + switch (fmt) { + default: return -1; + break; case 'c': case 'b': fscanf(fp, "%c,", (uint8_t*)addr8); + break; case 'w': fscanf(fp, "%#04llx,", (uint16_t*)addr8); + break; case 'i': case 'u': fscanf(fp, "%#08llx,", (uint32_t*)addr8); + break; case 'l': case 'q': fscanf(fp, "%#16llx,", (uint64_t*)addr8); + break; case 'f': fscanf(fp, "%f,", (float*)addr8); + break; case 'd': fscanf(fp, "%llf,", (double*)addr8); + break; case 's': fscanf(fp, "%[^\x1],", (char*)addr8); + } + else + switch(fmt) { + default: return -1; + break; case 'c': case 'b': fprintf(fp, "%c,", (int)*(uint8_t*)addr8); + break; case 'w': fprintf(fp, "%#04llx,", (uint64_t)*(uint16_t*)addr8); + break; case 'i': case 'u': fprintf(fp, "%#08llx,", (uint64_t)*(uint32_t*)addr8); + break; case 'l': case 'q': fprintf(fp, "%#16llx,", (uint64_t)*(uint64_t*)addr8); + break; case 'f': fprintf(fp, "%f,", (float)*(float*)addr8); + break; case 'd': fprintf(fp, "%f,", (double)*(double*)addr8); + break; case 's': fprintf(fp, "%s\x1,", *(char**)addr8); + } + addr8 += slot; + iterated_bytes += slot; + processed_bytes += size; + } + return iterated_bytes; +} + +static size_operator fsize_fn = size_fn; +static call_operator fcall_fn = call_fn; + +void foverride(size_operator size, call_operator call) { + fsize_fn = size; + fcall_fn = call; +} + +int fdump(FILE *out, void *addr, const char *dna, int rd) { + unsigned pragma_pack = pragma_pack_alignment; pragma_pack_alignment = 0; // reset alignment + uint8_t *addr8 = (uint8_t*)addr; + int size = 0, count = 1, skip = 0; + int last_type = 0; + int written = 0; + int align = 0; + for( int i = 0; dna[i]; ++i) { + char fmt = dna[i]; + /**/ if(fmt <= 32) continue; + else if(fmt >= '0' && fmt <= '9') continue; + else if(fmt == 'z') skip = 1; + else { + + // member alignment + if( last_type != fmt ) { // check if next struct member was found (may need re-alignment) + if( pragma_pack != 1 ) { // forced (>1) or auto-alignment (0)? + //printf("*%p ->", addr8); + align = pragma_pack == 0 ? fsize_fn(fmt, 0, rd) : pragma_pack; + // Round up to N-byte boundary + addr8 = (uint8_t*)(((uintptr_t)(addr8) + ((align) - 1)) & -(align)); + //printf(" %p\n", addr8); + } + } + + last_type = fmt; + + size = fsize_fn(fmt, addr8, rd); + if( size < 0 ) { + fprintf(stderr, "parse error, unknown dna sequence '%c'!\n", fmt); + return -i-1; + } + } + + if( skip ) { skip = 0; continue; } + + char next = dna[i+1]; + if( next >= '0' && next <= '9' ) { + count = next - '0'; + } + int bytes = skip || count == 0 ? 0 : call_fn(out, addr8, dna[i], count, rd); + if( bytes < 0 ) { + fprintf(stderr, "stream fail. rc: %d\n", bytes); + return bytes; + } + written += bytes; + addr8 += bytes; + count = 1; + + fprintf(out, "\n"); + } + return written; +} + +int fsave(FILE *out, const void *structure, const char *dna) { + return fdump(out, (void*)structure, dna, 0); +} +int fload(FILE *inf, void *structure, const char *dna) { + return fdump(inf, structure, dna, 1); +} +int fsavefile(const char *outfile, const void *structure, const char *dna) { + FILE *fp = fopen(outfile, "wb"); + if( !fp ) return 0; + int bytes = fdump(fp, (void*)structure, dna, 0); + fclose(fp); + return bytes; +} +int floadfile(const char *infile, void *structure, const char *dna) { + FILE *fp = fopen(infile, "rb"); + if( !fp ) return 0; + int bytes = fdump(fp, (void*)structure, dna, 1); + fclose(fp); + return bytes; +} + +// --- + +#include + + +// #pragma pack(1) + +struct fat_mbr { + uint8_t jmp[3]; // b3 + uint8_t oem[8]; // b8 + const char* str; // s + uint16_t bytes_per_sector; // w + uint8_t sectors_per_cluster; // b + uint16_t reserved_sectors; // w + uint8_t fat_copies; // b + uint16_t max_dirs; // w + uint16_t sector_count; // w + uint8_t media_descriptor; // b + uint16_t sectors_per_fat; // w + uint16_t sectors_per_head; // w + uint16_t heads; // w + uint32_t hidden_sectors; // u + uint32_t sector_count2; // u + float pi; + char break_alignment; // b + double phi; +}; + + #define FAT_MBR_DNA "b3b8s wb wb w wb www uu fbd" + +// #pragma pack(pop) + + +int main() { + +// foverride(size_fn, write_fn); + + struct fat_mbr mbr = { + {'a','b','c'}, + {'d','e','f','g','h','i','j','k'}, + "hello 'escaped' \"world\"", + 0x100,'l', + 0x101,'m', + 0x102, + 0x103,'n', + 0x104, + 0x105, + 0x106, + 0x01234567, + 0x89abcdef, + 3.14159f,'o', + 1.6069, + }; + +// fdump(stdout, &mbr, FAT_MBR_DNA); exit(0); +// printf("%p\n", &mbr.jmp); +// printf("%p\n", &mbr.oem); +// printf("%p\n", &mbr.str); +// printf("%p\n", &mbr.sector_count); + +// falign(0); + int bytes = fsave(stdout, &mbr, FAT_MBR_DNA); + printf("%d bytes written\n", bytes); + + + typedef struct entitystate { + struct pos { + short trTime; + float trBase[3]; + } pos; + } entitystate; + entitystate e = { 123, 3.14159f, 4.14159f, 5.14159f }; + bytes = fsave(stdout, &e, "wfff"); + printf("%d bytes written\n", bytes); + + +// exit(0); + + struct fat_mbr zero = {0}; + struct fat_mbr src = mbr; + struct fat_mbr dst = zero; + + dst = src; + assert(0 == memcmp(&src,&dst,sizeof(struct fat_mbr))); + + dst = zero; + assert(0 != memcmp(&src,&dst,sizeof(struct fat_mbr))); + + int sbytes = fsavefile(".temp", &src, FAT_MBR_DNA); + int lbytes = floadfile(".temp", &dst, FAT_MBR_DNA); + assert( sbytes == lbytes ); + assert(0 != memcmp(&src,&dst,sizeof(struct fat_mbr))); + + assert(!puts("Ok")); +} diff --git a/tools/editor/labs.meta/meta_info.c b/tools/editor/labs.meta/meta_info.c new file mode 100644 index 0000000..32d1b22 --- /dev/null +++ b/tools/editor/labs.meta/meta_info.c @@ -0,0 +1,292 @@ +//#define META_DEMO + +#include "fwk.h" + +#include +#include +#include +#include +#include +#include + +static +char *reformat(const char *text, const char *blacklist, const char *separator) { + char **list = strsplit(text, blacklist); + char *joint = strjoin(list, separator); + return joint; +} + +const char *c_symbols = "{},>&/+=;:"; // "(){}[].,><&*/+=;:!~"; +const char *c_symbols_extra = "{},>&/+=;:[]()"; // "(){}[].,><&*/+=;:!~"; +const char *c_keywords[] = { "const","long","signed","unsigned","typedef" }; int num_keywords = 4; + +typedef struct meta { + const char *name; + const char *tags; +} meta; +meta metas[2048] = { +/*00*/ {"typedef", ""}, +/*01*/ {"enum", ""}, +/*02*/ {"struct", ""}, +/*03*/ {"function", ""}, + +/*04*/ {"bool", "default 0"}, +/*05*/ {"false", "default 0 bits 1"}, +/*06*/ {"true", "default 1 bits 1"}, + +/*07*/ {"char", "default 0 bits 8"}, +/*08*/ {"int", "default 0"}, +/*09*/ {"float", "default 0 bits 32"}, +/*10*/ {"double", "default 0 bits 64"}, + +/*11*/ {"int8_t", "default 0 bits 8"}, +/*12*/ {"int16_t", "default 0 bits 16"}, +/*13*/ {"int32_t", "default 0 bits 32"}, +/*14*/ {"int64_t", "default 0 bits 64"}, +/*15*/ {"uint8_t", "default 0 bits 8 signed 0"}, +/*16*/ {"uint16_t", "default 0 bits 16 signed 0"}, +/*17*/ {"uint32_t", "default 0 bits 32 signed 0"}, +/*18*/ {"uint64_t", "default 0 bits 64 signed 0"}, +}; +int meta_count = 19; +const char *meta_last_name = 0; +const char *meta_last_struct = 0; + +meta* meta_find(const char *name) { + for( int i = 0; i < meta_count; ++i ) { + if( 0 == strcmp(name, metas[i].name) ) return &metas[i]; + } + return 0; +} +meta* meta_add(const char *name, const char *subtype, const char *tags ) { + { meta *t = meta_find(name); if( t ) return t; } + + if( meta_count >= 2048 ) return 0; + + meta t = { STRDUP(name), reformat(STRDUP(tags), " ", " ") }; + metas[meta_count] = t; + meta_last_struct = !strcmp(subtype, "struct") ? name : meta_last_struct; + meta_last_name = !strcmp(subtype, "struct") || !strcmp(subtype, "enum") || !strcmp(subtype, "typedef") ? name : subtype; + return &metas[meta_count++]; +} +char* meta_find_value(const char *name, const char *prop) { + meta *t = meta_find(name); + if( !t ) return 0; + char *found = strstr(t->tags, prop); + if( !found ) return 0; + + static char copy[256] = {0}; // @fixme + strcpy(copy, found + strlen(prop) + 1); + for(int i=0; copy[i]; ++i) if(copy[i]==' ') { copy[i] = 0; break; } + return copy; +} +void meta_inspectf(FILE *fp) { + if( fp ) { + char buf[255+1]; + + while(fgets(buf, 255, fp)) { + char *mark = strstr(buf, "//""M"); + if( !mark ) continue; + char *tags = mark + 3; + + // must reset last_struct + if( strstr(buf, "typedef") ) { + meta_last_struct = 0; // typedef is a special case + } + + // remove symbols + for( int i = 0; buf[i]; ++i ) { + // symbols off + if(strchr(&buf[i] < tags ? c_symbols : c_symbols_extra, buf[i])) { buf[i] = ' '; continue; } + } + + // remove leading whitespaces + char *spc = buf; + while( spc && spc[0] && spc[0] <= 32 ) ++spc; + + // remove trailing linefeeds and whitespaces + int len = strlen(buf); + while( len > 0 && buf[len-1] <= 32 ) buf[--len] = 0; + + // split buffer into left|right buffers + char *left = spc, *right = tags; right[-1] = 0; + + // remove left keywords + for( int i = 0; left[i]; ++i ) { + for(int j = 0; j < sizeof(c_keywords)/sizeof(0[c_keywords]); ++j) { + int kb = strlen(left+i); + int kl = strlen(c_keywords[j]); + if( kl <= kb && !memcmp(left+i, c_keywords[j], kl) ) { + memset(left+i, ' ', kl); + i += kl; + break; + } + } + } + + // debug + // printf("%s <--> %s\n", left, right); + + // add type + const char *meta_name = 0; + const char *meta_t = 0; + const char *meta_val = ""; + array(char *) tokens = strsplit(left, " \t"); + if(1) for( int i = 0, end = array_count(tokens); i < end; ++i ) { + /**/ if( strstr(tokens[i],"enum") ) meta_last_name = 0, meta_last_struct = 0, meta_t = "enum"; + else if( strstr(tokens[i],"struct") ) meta_last_name = 0, meta_last_struct = 0, meta_t = "struct"; + else if( strstr(tokens[i],"typedef") ) meta_last_name = 0, meta_last_struct = 0, meta_t = "typedef"; // trimmed. never happens + else if( strchr(tokens[i],'(') ) meta_last_name = 0, meta_last_struct = 0, meta_name = tokens[i], meta_t = "function"; + else if( !meta_name ) meta_name = tokens[i]; + else if( !meta_t ) meta_t = tokens[i]; + } + + if( meta_find(meta_name) ) { + const char *swap = meta_name; + meta_name = meta_t; + meta_t = swap; + } + + // if !meta_find defer(meta_eval) && emit(warning) && keep(iterating); + + bool is_pointer = strstr(meta_name, "*"); + meta_name = (char *)reformat(meta_name, "*", ""); + char *found_sqr = strstr(meta_name, "["); + int array_len = found_sqr ? atoi(found_sqr+1) : 0; + if( found_sqr ) *found_sqr = 0; + + bool is_number = false; + char conv[32] = {0}; + double dbl = atof(meta_t); + if(!is_number) { sprintf(conv, "%f", dbl); is_number = !strcmp(conv, meta_t); } + if(!is_number) { sprintf(conv, "%.f", dbl); is_number = !strcmp(conv, meta_t); } + if(is_number) { + meta_val = meta_t; + meta_t = meta_last_name; + } + + char hints[256] = {0}, *p = hints; + if( 1 ) p += sprintf(p, "type %s ", meta_t); + if( is_pointer || array_len ) p += sprintf(p, "pointer %d ", 1); + if( array_len ) p += sprintf(p, "count %d ", array_len); + if( 1 ) p += sprintf(p, "%s ", tags); + if( meta_last_struct ) p += sprintf(p, "struct %s ", meta_last_struct); + if( meta_val[0] ) p += sprintf(p, "default %s ", meta_val); + meta_val = hints; + + // inherited tags (from super type) at the very end, so overriden tags can be found at first place + if( meta_find(meta_t) ) + p += sprintf(p, "%s ", meta_find(meta_t)->tags ); + + + if( !meta_find(meta_name) ) { + meta_add(meta_name, meta_t, meta_val); + + meta *t = meta_find(meta_name); + //printf("%s,%s\n", t->name, t->tags); + } + + } + // clean state + meta_last_name = 0; + meta_last_struct = 0; + } +} + +bool meta_save(FILE *fp) { + if( fp ) + for( int i = 0; i < meta_count; ++i ) { + meta *t = &metas[i]; + fprintf(fp, "%s %s\n", t->name, t->tags); + } + return !!fp; +} +bool meta_load(FILE *fp) { + if( fp ) { + meta_count = 0; + meta_last_name = 0; + meta_last_struct = 0; + + char buf[2048+1]; + while(fgets(buf, 2048, fp)) { + int bl = strlen(buf); while( bl && buf[bl] <= 32) buf[bl--] = 0; + char id[128]; + sscanf(buf, "%s", id); + metas[ meta_count ].name = strdup(id); + metas[ meta_count ].tags = strdup(buf+strlen(id)+1); + ++meta_count; + } + } + return !!fp; +} + +void meta_inspect(const char *name) { + FILE *fp = fopen(name, "rb"); + if( !fp ) return; + meta_inspectf(fp); + fclose(fp); +} +bool meta_savefile(const char *name) { + FILE *fp = fopen(name, "wb"); + if( !fp ) return false; + meta_save(fp); fclose(fp); + return true; +} +bool meta_loadfile(const char *name) { + FILE *fp = fopen(name, "rb"); + if( !fp ) return false; + meta_load(fp); fclose(fp); + return true; +} + + +#ifdef META_DEMO +#pragma once + +typedef int my_array[8]; //M +typedef const char *string; //M + +typedef enum Fruit { //M bits:8 + Banana = -1, //M + Orange = 42 //M +} Fruit; + +struct myObject { //M version:100 + Fruit meal; //M default:Banana + int32_t hitpoints; //M default:100 zigzag:yes + float shininess; //M default:+3.5e2 min:0 max:1 fixed:yes + string text; //M default:"DEMO" + bool visible; //M bits:1 deprecated:1 version:001 + char test; //M serialize:off +}; + +int print(int); //M const:true + +typedef int dummy; //M extra=separators allowed(too) + +int main(int argc, const char **argv) { + +#ifndef NDEBUG + unlink(__FILE__ ".meta"); +#endif + + if( meta_loadfile(__FILE__ ".meta") ) { + puts("Loaded .meta file"); + } else { + puts("Cannot read .meta file. regenerating..."); + meta_inspect(__FILE__); + puts("Saving .meta file..."); + meta_savefile(__FILE__ ".meta"); + } + + meta_save( stdout ); + + puts(meta_find_value("Orange", "type")); + puts(meta_find_value("Orange", "bits")); + puts(meta_find_value("Orange", "default")); + puts(meta_find_value("visible", "type")); + puts(meta_find_value("shininess", "fixed")); +} + +#define main main__ +#endif diff --git a/tools/editor/labs.meta/meta_reflect.c b/tools/editor/labs.meta/meta_reflect.c new file mode 100644 index 0000000..2669701 --- /dev/null +++ b/tools/editor/labs.meta/meta_reflect.c @@ -0,0 +1,386 @@ +// missing //M(eta), DNA, +// meta_parse(file), meta_load, meta_save + +#define REFLECT_C +//#define REFLECT_DEMO + +// C reflection: enums, functions, structs, nested structs, members and pointers. +// - rlyeh, public domain + +#ifndef REFLECT_H +#define REFLECT_H + +// # reflection api +// +// - reflected symbol struct +// +// - define reflected symbol (3 quick macros). +// - define reflected symbol (complete function). +// +// - size of reflected symbol. +// - find reflected function (by name). +// - find reflected enum (by name). +// - find reflected field in struct (by name). +// +// - iterate all reflected fields in struct. +// - iterate all reflected symbols in registry. +// +// - @todo: reflect* reflect_find() +// - @todo: code annotations? "display-name", "min", "max", "range", "default" +// - @todo: declare TYPEDEF(vec3, float[3]), TYPEDEF(mat4, vec4[4]/*float[16]*/) + +typedef struct reflect { + union { + void *any; + int offs; + }; + const char *type, *name, *base, *info; + unsigned size:23, is_pod:1, is_ptr:6, internal_type:2; +} reflect; + +#define ENUM(type, name, ...) reflect_add(0, (void*)name, #type, #name, "", "" #__VA_ARGS__ "\0", sizeof(enum type)) +#define FUNCTION(type, name, ...) reflect_add(1, &name, #type, #name, "", "" #__VA_ARGS__ "\0", sizeof(void *)) +#define STRUCT(struct, type, name, ...) reflect_add(2, &(((struct *)0)->name), #type, #name, #struct, "" #__VA_ARGS__ "\0", (int)sizeof(type) ) + +void reflect_add( int internal_type, void *any, const char *type, const char *name, const char *base, const char *info, int size ); + +int reflect_sizeof( const char *type ); +void* reflect_function( const char *name ); +int reflect_enum( const char *name ); +void* reflect_field( const char *type, void *obj, const char *name, const char **opt_type ); + +bool reflect_has_fields( const char *base, void *obj ); +void reflect_iterate_fields( const char *type, void *obj, void (*callback)( const reflect *r, void *value, void *userdata ), void *userdata ); +void reflect_iterate_registry( void (*callback)( const reflect *r, void *userdata ), void *userdata ); + +#endif + +// ---------------------------------------------------------------------------- + +#ifdef REFLECT_C +#pragma once +#include +#include +#include + +#ifndef REFLECT_REGISTRY_LIMIT +#define REFLECT_REGISTRY_LIMIT 0 // 0 for unlimited registry size (using heap), or N for fixed max entries (using stack) +#endif + +#if REFLECT_REGISTRY_LIMIT > 0 +static reflect *registry[REFLECT_REGISTRY_LIMIT] = {0}, registry_[REFLECT_REGISTRY_LIMIT] = {0}; +#else +static reflect **registry = 0; +#endif + +enum { + REFLECT_TYPE_ENUM, + REFLECT_TYPE_FUNCTION, + REFLECT_TYPE_FIELD, +}; + +static int reflect_counter = 0; + +void reflect_add( int internal_type, void *any, const char *type, const char *name, const char *base, const char *info, int size ) { + ++reflect_counter; + +#if REFLECT_REGISTRY_LIMIT > 0 + registry[reflect_counter-1] = ®istry_[reflect_counter-1]; +#else + registry = (reflect **)realloc( registry, reflect_counter * sizeof(reflect *)); + registry[reflect_counter-1] = (reflect *)malloc( sizeof(reflect) ); +#endif + + reflect s = { any, type, name, base, info, size, 1, 0, internal_type }; + + reflect *r = registry[reflect_counter-1]; + *r = s; + + // evaluate is_ptr + for( int i = strlen(r->type); !r->is_ptr && --i; ) { + r->is_ptr = r->type[i] == '*' ? i : 0; + } + for( int i = r->is_ptr; i >= 0; --i ) { + r->is_ptr = r->type[i] == ' ' ? i : r->is_ptr; + } + + // @fixme: try to avoid dynamic allocs + // future me: removing this will break some strcmps(type) below + if( r->is_ptr ) { + char buf[128]; sprintf(buf, "%.*s", r->is_ptr, r->type); + r->type = (const char *)strdup(buf); + } + + // evaluate is_pod. kind of bubble sort. + for( int i = 0; i < reflect_counter; ++i ) { + for( int j = 0; j < reflect_counter; ++j ) { + if( !strcmp(registry[i]->base, registry[j]->type) ) { + registry[j]->is_pod = 0; + } + } + } +} + +int reflect_sizeof( const char *base ) { + int size = 0; + if( base ) for( int i = 0; i < reflect_counter; ++i ) { + if( !strcmp(registry[i]->base, base) ) size += registry[i]->size; + } + return size; +} + +void *reflect_field( const char *base, void *obj, const char *name, const char **opt_type ) { + if( base ) for( int i = 0; i < reflect_counter; ++i ) { + if( registry[i]->internal_type == REFLECT_TYPE_FIELD && strcmp(registry[i]->base, base) ) continue; + if(!strcmp(registry[i]->name, name) ) return (opt_type ? *opt_type = registry[i]->type : NULL), (char*)obj + (long long int)registry[i]->any; + } + return 0; +} + +int reflect_enum( const char *name ) { + if( name ) for( int i = 0; i < reflect_counter; ++i ) { + if( registry[i]->internal_type == REFLECT_TYPE_ENUM && !strcmp( registry[i]->name, name ) ) { + return (int)(long long int)registry[i]->any; + } + } + return 0; +} + +// do not use void(void) signature here +static void* reflect_dummy_call() { + return 0; +} + +void* reflect_function( const char *name ) { + if( name ) for( int i = 0; i < reflect_counter; ++i ) { + if( registry[i]->internal_type == REFLECT_TYPE_FUNCTION && !strcmp( registry[i]->name, name ) ) { + return registry[i]->any; + } + } + return &reflect_dummy_call; // return NULL instead? +} + +void reflect_iterate_registry( void (*callback)( const reflect *r, void *userdata ), void *userdata ) { + for( int i = 0; i < reflect_counter; ++i ) { + callback( registry[i], userdata ); + } +} + +#ifdef _MSC_VER +__declspec(thread) +#else +__thread +#endif +struct reflect_context { + const char *name; + const char *info; +} reflect_ctx = { "", "" }; + +void reflect_iterate_fields( const char *base, void *obj, void (*callback)( const reflect *r, void *value, void *userdata ), void *userdata ) { + if( base ) for( int i = 0, nb = strlen(base); i < reflect_counter; ++i ) { + if( registry[i]->internal_type == REFLECT_TYPE_FIELD && !strncmp(registry[i]->base, base, nb) ) { + for( ; i < reflect_counter && !strncmp(registry[i]->base, base, nb); ++i ) { + void *any = ((char*)obj + (long long int)registry[i]->any); + + if( any && registry[i]->is_ptr ) { + any = (void*)*((long long int *)any); + } + + if( any ) { + struct reflect_context copy = reflect_ctx; + char buf1[128]; sprintf(buf1, "%s.%s", reflect_ctx.name, registry[i]->name); + char buf2[128] = {0}; if( registry[i]->info[0] ) sprintf(buf2, "%s%s%.*s", reflect_ctx.info, reflect_ctx.info[0] ? " > " : "", (int)(strlen(registry[i]->info) - 2), 1+registry[i]->info); + reflect_ctx.name = buf1 + (buf1[0] == '.'); + reflect_ctx.info = buf2; + + if( registry[i]->is_pod ) { + reflect m = *registry[i]; + m.name = reflect_ctx.name; + m.info = reflect_ctx.info; + callback( &m, any, userdata ); + } else { + char buf3[128], *rebase = (char*)registry[i]->type; + if(registry[i]->is_ptr) sprintf( rebase = buf3, "%.*s", registry[i]->is_ptr, registry[i]->type ); + reflect_iterate_fields( rebase, any, callback, userdata ); + } + + reflect_ctx = copy; + } + } + return; + } + } +} + +bool reflect_has_fields( const char *base, void *obj ) { + if( base ) for( int i = 0, nb = strlen(base); i < reflect_counter; ++i ) { + if( registry[i]->internal_type == REFLECT_TYPE_FIELD && !strncmp(registry[i]->base, base, nb) ) { + for( ; i < reflect_counter && !strncmp(registry[i]->base, base, nb); ++i ) { + return true; + } + } + } + return false; +} + +void reflect_dump_registry(FILE *fp) { + fprintf( fp, "%s {\n", __FUNCTION__ ); + fprintf( fp, "\tsizeof(reflect)=%d\n", (int)sizeof(reflect) ); + + for( int i = 0; i < reflect_counter; ++i ) { + reflect *r = registry[i]; + + /**/ if( r->internal_type == REFLECT_TYPE_FUNCTION ) + fprintf(fp, "\tfunction %s = %s; // %d@[%p] %s\n", r->name, r->type, r->size, r->any, r->info); + else if( r->internal_type == REFLECT_TYPE_ENUM ) + fprintf(fp, "\tenum %s%s%s = %d; // %d %s\n", r->type[0] ? r->type : "", r->type[0] ? "." : "", r->name, (int)(long long int)r->any, r->size, r->info ); + else if( r->internal_type == REFLECT_TYPE_FIELD ) + fprintf(fp, "\t%s%s%s %s.%s; // %d@%d %s\n", r->is_pod ? "field " : "struct ", r->type, r->is_ptr ? "*" : "", r->base, r->name, r->size, (int)(long long int)r->any, r->info ); + } + + fprintf( fp, "} %s\n", __FUNCTION__ ); +} + +#endif + +// ---------------------------------------------------------------------------- + +#ifdef META_DEMO_IMMEDIATE +#include +#include +#include +#include + +void echo( const reflect *r, void *value, void *userdata ) { + FILE *fp = (FILE*)userdata; + if( fp ) { + /**/ if( !strcmp(r->type, "int") ) fprintf(fp, "%6s %-32s = %d;\t// %s\n", r->type, r->name, *(int*)value, r->info); + else if( !strcmp(r->type, "string") ) fprintf(fp, "%6s %-32s = %s;\t// %s\n", r->type, r->name, *(char**)value, r->info); + else if( !strcmp(r->type, "float") ) fprintf(fp, "%6s %-32s = %f;\t// %s\n", r->type, r->name, *(float*)value, r->info); + else if( !strcmp(r->type, "double") ) fprintf(fp, "%6s %-32s = %f;\t// %s\n", r->type, r->name, *(double*)value, r->info); + } +} + +enum MY_ENUM { + TEXTURE = 101, + IMAGE = 102, +}; + +typedef char* string; + +typedef struct MyVec3 { + double x,y,z; +} MyVec3; + +typedef struct MyTransform { + MyVec3 location; + MyVec3 rotation; + float scale; +} MyTransform; + +typedef struct MyObject { + struct MyObject *parent; + string id; + int hash; + MyTransform transform; +} MyObject; + +int MyAddFunction( int a,int b ) { + return a + b; +} + + +int main( int argc, char **argv ) { + + // # enums + // - register + // - reflect + + ENUM( MY_ENUM, TEXTURE ); + ENUM( MY_ENUM, IMAGE ); + + assert( 101 == reflect_enum("TEXTURE") ); + assert( 102 == reflect_enum("IMAGE") ); + + // # functions + // - register + // - find + // - call + // - try call (undefined functions are also safe to call) + + FUNCTION( int(int a, int b), MyAddFunction, "Function adding two numbers" ); + FUNCTION( int(const char *fmt, ...), printf, "Print text to console" ); + + int (*add_hook)() = reflect_function("MyAddFunction"); + int (*print_hook)(const char *, ...) = reflect_function("printf"); + + assert( 123 == add_hook(100,23) ); + print_hook("hello from reflected function %d\n", 123); + + print_hook = reflect_function("undefined_symbol_here$(·!!"); + print_hook("this call should never print\n"); + puts("---"); + + // # structs + // - register simple + // - register nested + // - register nested with pointers + // - print struct sizes + // - iterate simple + // - iterate nested + // - iterate nested with pointers + + STRUCT( MyVec3, double, x, "Right" ); + STRUCT( MyVec3, double, y, "Forward" ); + STRUCT( MyVec3, double, z, "Up" ); + + STRUCT( MyTransform, MyVec3, location, "World location (absolute)" ); + STRUCT( MyTransform, MyVec3, rotation, "Local rotation (in degrees)" ); + STRUCT( MyTransform, float, scale, "Local scale (in centimeters)" ); + + STRUCT( MyObject, int, hash, "Actor hash" ); + STRUCT( MyObject, string, id, "Actor name" ); + STRUCT( MyObject, MyTransform, transform, "Actor transform" ); + STRUCT( MyObject, MyObject *, parent, "Actor parent" ); + + printf("reflect_sizeof(MyVec3)=%d\n", reflect_sizeof("MyVec3")); + printf("reflect_sizeof(MyTransform)=%d\n", reflect_sizeof("MyTransform")); + printf("reflect_sizeof(MyObject)=%d\n", reflect_sizeof("MyObject")); + puts("---"); + + MyVec3 vec = {1.1,2.2,3.3}; + reflect_iterate_fields( "MyVec3", &vec, echo, stdout ); + puts("---"); + + MyTransform tf = { {1.1,2.2,3.3}, {45,90,180}, 10 }; + reflect_iterate_fields( "MyTransform", &tf, echo, stdout ); + puts("---"); + + MyObject root = { NULL, "Scene root", 0, { {0,0,0}, {0,0,0}, 1 } }; + MyObject obj = { &root, "Name identifier", 123, { {1.1,2.2,3.3}, {45,90,180}, 10 } }; + reflect_iterate_fields( "MyObject", &obj, echo, stdout ); + puts("---"); + + // # dump internals + + reflect_dump_registry(stdout); + puts("---"); + + // # benchmark + + #ifndef N + #define N (argc > 1 ? atoi(argv[1]) : 500 * 1000) + #endif + + clock_t t0 = clock(); + for( int i = 0; i < N; ++i) { + reflect_iterate_fields( "MyObject", &obj, echo, NULL ); + } + clock_t t1 = clock(); + + double t = (t1 - t0) / (double)CLOCKS_PER_SEC; + printf("Benchmark: processed %.f members in %5.2fs = %5.2f members/sec\n", N*9.0, t, (N*9.0)/t ); // 9 total members in MyObject +} + +#endif + diff --git a/tools/editor/labs.meta/meta_tool.c b/tools/editor/labs.meta/meta_tool.c new file mode 100644 index 0000000..a381339 --- /dev/null +++ b/tools/editor/labs.meta/meta_tool.c @@ -0,0 +1,46 @@ +#define FWK_C +#include "fwk.h" + +bool parse_struct(const char *line) { + return strstr(line, "s""truct "); +} +bool parse_union(const char *line) { + return strstr(line, "u""nion"); +} +bool parse_enum(const char *line) { + return strstr(line, "e""num "); +} +bool parse_typedef(const char *line) { + return strstr(line, "t""ypedef "); +} +bool parse_function(const char *line) { + return strstr(line, "(") && strstr(line, ");"); +} +bool parse_comment(const char *line) { + return strstr(line, "//") || (strstr(line, "/*") && strstr(line, "*/")); +} +bool parse_variable(const char *line) { + return strstr(line, "="); +} +bool parse_member(const char *line) { + return strstr(line, ";"); +} +const char* parse_any(const char *line) { + if(parse_struct(line)) return "STRUCT"; + if(parse_union(line)) return "UNION"; + if(parse_enum(line)) return "ENUM"; + if(parse_typedef(line)) return "TYPEDEF"; + if(parse_variable(line)) return "VARIABLE"; + if(parse_function(line)) return "FUNCTION"; + if(parse_member(line)) return "MEMBER"; + return 0; +} + +int main() { + char *data = file_read(__FILE__); + + for each_substring(data, "\r\n", line) { + const char *type = parse_any(line); + printf("%s%s%s\n", line, type ? " -> " : "", type ? type : ""); + } +} diff --git a/tools/editor/labs.osc/MAKE.bat b/tools/editor/labs.osc/MAKE.bat new file mode 100644 index 0000000..c7ffcd0 --- /dev/null +++ b/tools/editor/labs.osc/MAKE.bat @@ -0,0 +1,42 @@ +@if "%1" == "tidy" ( + del *.zip + del *.mem + del *.exp + del *.lib + del *.exe + del *.obj + del *.o + del *.a + del *.pdb + del *.ilk + del *.def + del *.dll + del oscedit.ini + rd /q /s .vs + exit /b +) + +cl ..\editor2.c -I ..\..\tools -DCOOK_ON_DEMAND + +pushd ..\.. && call make amalgamation && popd + +taskkill /im "oscedit.exe" > nul 2> nul +call ..\..\tools\tcc oscgame.c -I ..\.. -DFWK_IMPLEMENTATION -DCOOK_ON_DEMAND %* +call ..\..\tools\tcc oscsend.c -I ..\.. -DFWK_IMPLEMENTATION -DCOOK_ON_DEMAND %* +call ..\..\tools\tcc oscedit.c -I ..\.. -DFWK_IMPLEMENTATION -DCOOK_ON_DEMAND %* && start oscedit.exe + +timeout 3 + +:: showcase UI widgets creation on demand, which are requested from this very same batch file +oscsend /player/time 5.5 +oscsend /player/name "john doe" +oscsend /player/health 100 +oscsend /player/is_active true + +oscsend /player2/integer 123 +oscsend /player2/string world + +oscsend /player1/string hello + +:: simulate a game running sending OSC commands to our editor +rem oscgame diff --git a/tools/editor/labs.osc/oscedit.c b/tools/editor/labs.osc/oscedit.c new file mode 100644 index 0000000..774d222 --- /dev/null +++ b/tools/editor/labs.osc/oscedit.c @@ -0,0 +1,22 @@ +#include "fwk.h" +#include "oscedit.h" + +// demo + +int main() { + do_once map_init(client_vars, less_str, hash_str); + do_once map_init(server_vars, less_str, hash_str); + + int server_socket = osc_listen("0.0.0.0", OSC_EDIT_PORT); + if( server_socket >= 0 ) { + if( !strcmp("--launch", argv(1)) ) system("oscgame"); // launch game + + cook_config("../../tools/cook.ini"); + + window_create(0.80, WINDOW_SQUARE); + window_title("EDITOR"); + while( window_swap() ) { + osc_edit_sync(server_socket, -1, 4); + } + } +} diff --git a/tools/editor/labs.osc/oscedit.h b/tools/editor/labs.osc/oscedit.h new file mode 100644 index 0000000..e96da08 --- /dev/null +++ b/tools/editor/labs.osc/oscedit.h @@ -0,0 +1,221 @@ +// public api + +int osc_edit(const char *hierarchy_descriptor, char type, void *ptr); // descriptor format: [ifsTF]/path/name +void osc_edit_sync(int server_fd, int client_fd, unsigned timeout_ms); + +int osc_edit_load(const char *mask); +int osc_edit_save(const char *mask); +int osc_edit_reset(const char *mask); + +#define OSC_EDIT_PORT "2023" // maybe use portname("OSC_EDITOR_V1", 000) ? + +// --- impl + +#pragma once +#include "oscpack.h" +#include "oscsend.h" +#include "oscrecv.h" + +#ifndef OSC_EDIT_INI +#define OSC_EDIT_INI "oscedit.ini" +#endif + +typedef struct osc_variant { + char *key; // key address + char type; // variant type + union osc_variant_ { + int64_t i; + char *s; + double f; + uintptr_t up; + } live[1], offline[1]; + bool edited; +} osc_variant; + +map(char*, void*) client_vars; +array(char*) client_outgoing; + +map(char*, osc_variant) server_vars; +array(char*) server_outgoing; + +int osc_edit(const char *hierarchy_descriptor, char type, void *ptr) { + *((void**)map_find_or_add(client_vars, (char*)hierarchy_descriptor, ptr)) = ptr; + array_push(client_outgoing, stringf("%c%s", type, hierarchy_descriptor)); // @leak + return 0; +} + +int osc_edit_load(const char *mask) { + ini_t map = ini(OSC_EDIT_INI); + if( !map ) return 0; + + map_clear(server_vars); // @todo: @leak: iterate variant strings and free() them + + for each_map_ptr_sorted(map, char *, key, char *, val) { + if( strmatchi(*key + 2, mask ) ) { // skip initial char_type+dot + printf("%s=%s\n", *key, *val); + + osc_variant variant = {0}; + variant.type = 0[*key]; + + /**/ if( variant.type == 'i' ) variant.live[0].i = atoi(*val); + else if( variant.type == 'f' ) variant.live[0].f = atof(*val); + else if( variant.type == 's' ) variant.live[0].s = STRDUP(*val); + else if( strchr("bTF",variant.type) ) variant.live[0].i = (*val)[3] == 't'; + + variant.key = STRDUP(*key + 2); + variant.offline[0] = variant.live[0]; + + map_insert(server_vars, STRDUP(*key + 2), variant); // @todo: no need to STRDUP() here. we got variant.key already allocated. + } + } + + map_free(map); + + ui_notify("Loaded.", NULL); + return 1; +} + +int osc_edit_save(const char *mask) { + unlink( OSC_EDIT_INI ); + + for each_map_ptr(server_vars, char *, title, osc_variant, msg) { + if( strmatchi(*title, mask) ) { + msg->edited = false; + + if( msg->type == 's' ) msg->offline[0].s = STRDUP(msg->live[0].s); // @leak + else memcpy( &msg->offline[0], &msg->live[0], sizeof(msg->offline[0]) ); + + if( msg->type == 'i' ) ini_write(OSC_EDIT_INI, "i", msg->key, va("%d", (int)msg->live[0].i )); + if( msg->type == 'f' ) ini_write(OSC_EDIT_INI, "f", msg->key, va("%f", (float)msg->live[0].f )); + if( msg->type == 's' ) ini_write(OSC_EDIT_INI, "s", msg->key, va("%s", msg->live[0].s )); + if( strchr("bTF",msg->type) ) ini_write(OSC_EDIT_INI, "b", msg->key, va("%s", msg->live[0].i ? "true":"false" )); + } + } + + ui_notify("Saved.", NULL); + return 1; +} + +int osc_edit_reset(const char *mask) { + for each_map_ptr(server_vars, char *, title, osc_variant, msg) { + if( strmatchi(*title, mask) ) { + msg->edited = false; + + if( msg->type == 's' ) msg->live[0].s = STRDUP(msg->offline[0].s); // @leak + else memcpy( &msg->live[0], &msg->offline[0], sizeof(msg->live[0]) ); + } + } + return 1; +} + +void osc_edit_sync(int server_fd, int client_fd, unsigned timeout_ms) { + // client logic + if( client_fd >= 0 ) { + // 1. send outgoing vars (widget requests at server) + // 2. recv modified vars (from user tweaking widgets), wait till timeout_ms + // received vars are removed from outgoing queue, so it's never submitted again until it gets modified. + + // [1] + for(int i = 0; i < array_count(client_outgoing); ++i) { + char *out = 0; + /**/ if( client_outgoing[i][0] == 'i' ) out = osc_pack_va(client_outgoing[i]+1, "i", *(int*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); + else if( client_outgoing[i][0] == 'f' ) out = osc_pack_va(client_outgoing[i]+1, "f", *(float*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); + else if( client_outgoing[i][0] == 's' ) out = osc_pack_va(client_outgoing[i]+1, "s", *(char**)*((void**)map_find(client_vars, client_outgoing[i]+1)) ); + else if( client_outgoing[i][0] == 'b' ) out = osc_pack_va(client_outgoing[i]+1, *(bool*)*((void**)map_find(client_vars, client_outgoing[i]+1)) ? "T":"F" ); + else if( client_outgoing[i][0] == 'T' ) out = osc_pack_va(client_outgoing[i]+1, "T" ); + else if( client_outgoing[i][0] == 'F' ) out = osc_pack_va(client_outgoing[i]+1, "F" ); + else if( client_outgoing[i][0] == '\0') out = osc_pack_va(client_outgoing[i]+1, "" ); + else ASSERT(0, "unsupported osc_edit command `%c`", client_outgoing[i][0]); + if( out ) osc_send(client_fd, out+4, *(int*)out); + } + + for(int i = 0; i < array_count(client_outgoing); ++i) { + FREE(client_outgoing[i]); + } + + array_clear(client_outgoing); + + // [2] + sleep_ms(timeout_ms); + } + + // server logic + if( server_fd >= 0 ) { + // 1. recv vars from client + // 2. if they do not exist: instantiate UI controls for them + // 3. if they do exist: update values from client + // 4. when UI controls are modified, update our local copies and send its values back to client + + osc_update(server_fd, timeout_ms); // call every frame. reads the udp port and parse all messages found there + + const osc_message *begin; + for( int it = 0, end = osc_list(&begin); it < end; ++it ) { + const osc_message *msg = begin + it; + + osc_variant oscv, zero = {0}; + oscv.key = STRDUP(msg->pattern); + oscv.type = msg->types[0]; + if(oscv.type == 's') oscv.live[0].s = STRDUP(msg->v[0].s), oscv.offline[0].s = STRDUP(msg->v[0].s); + else memcpy(&oscv.live[0], &msg->v[0], sizeof(oscv.live[0])), memcpy(&oscv.offline[0], &msg->v[0], sizeof(oscv.live[0])); + + *map_find_or_add_allocated_key(server_vars, STRDUP(msg->pattern), zero) = oscv; + } + + static int on = 1; + if( ui_window("editor", &on) ) { + + int num_messages = osc_list(&begin); + int prev_title_len = 0; char *prev_title = 0; + + if( !map_count(server_vars) ) { + // create section header + int choice = ui_label2_toolbar("New", ICON_MD_LOOP); + if( choice == 1 ) osc_edit_load("*"); + } + else + for each_map_ptr_sorted(server_vars, char *, title, osc_variant, msg) { + + char *second_slash = strchr(*title + 1, '/'); + int title_len = (int)(second_slash - *title) - 1; // -> /player/title -> player + int different_size = title_len != prev_title_len; + int new_section = different_size || (prev_title && strncmp(prev_title, *title, title_len+1)); + + // create section header + if( new_section ) { + if( prev_title ) ui_separator(); + char *caption = va("*%.*s", title_len + 2, *title); + int choice = ui_label2_toolbar(caption, prev_title ? "" : va(">%d" ICON_MD_EMAIL " " ICON_MD_UNDO " " ICON_MD_LOOP " " ICON_MD_SD_CARD, num_messages)); // ICON_MD_UNDO " " ICON_MD_UPLOAD " " ICON_MD_DOWNLOAD + if( choice == 3 ) osc_edit_reset("*"); + if( choice == 2 ) osc_edit_load("*"); + if( choice == 1 ) osc_edit_save("*"); + prev_title_len = title_len, prev_title = va("%s", *title); + } else { + prev_title_len = title_len; + } + + ui_label_icon_highlight = !!msg->edited; +// vec2 ui_label_icon_clicked_L; // left +// vec2 ui_label_icon_clicked_R; // right + + char *title_copy = va(ICON_MD_UNDO " " "%s", *title + 1 + title_len + 1); // /player/titlexx -> UNDO_ICON titlexx + + // create gui elements + /**/ if( msg->type == '\0') { ui_label(title_copy); } + else if( msg->type == 'i' ) { int i = (int)msg->live[0].i; msg->edited |= ui_int(title_copy, &i); msg->live[0].i = i; } + else if( msg->type == 'f' ) { float f = msg->live[0].f; msg->edited |= ui_float(title_copy, &f); msg->live[0].f = f; } + else if( msg->type == 's' ) { char s[128] = {0}; strncpy(s, msg->live[0].s, sizeof(s)); uint64_t old = hash_str(s); ui_buffer(title_copy, s, sizeof(s)); uint64_t mod = hash_str(s); if(mod != old) msg->edited |= 1, FREE(msg->live[0].s), msg->live[0].s = stringf("%s",s); } + else if( strchr("bTF",msg->type) ) { bool b = !!msg->live[0].i; msg->edited |= ui_bool(title_copy, &b); msg->live[0].i = b; } + else ASSERT(0, "unsupported osc_edit command `%c`", msg->type); + + if( ui_label_icon_clicked_L.x > 0 && ui_label_icon_clicked_L.x <= 24 ) { // if clicked on UNDO icon (1st icon) + osc_edit_reset(*title); + } + } + + ui_window_end(); + } + + ui_demo(); + } +} + diff --git a/tools/editor/labs.osc/oscgame.c b/tools/editor/labs.osc/oscgame.c new file mode 100644 index 0000000..a8698ac --- /dev/null +++ b/tools/editor/labs.osc/oscgame.c @@ -0,0 +1,39 @@ +#include "fwk.h" +#include "oscedit.h" + +// game + +struct player { + char *name; + int health; + bool visible; + float time; +}; + +void render_game(struct player *p) { + printf("%*.s\rgame: %s health=%d time=%f visible=%d", 30, "", p->name, p->health, p->time, p->visible); +} + +// demo +int main() { + do_once map_init(client_vars, less_str, hash_str); + do_once map_init(server_vars, less_str, hash_str); + + int client_socket = osc_open("127.0.0.1", OSC_EDIT_PORT); + + if( client_socket >= 0 ) { + struct player P1 = { STRDUP("Player 1"), 100, true }; + + while( GetAsyncKeyState(VK_ESCAPE) & 0x8000 ^ 0x8000 ) { + P1.time = time_ss(); // << this is the "game", it just advances "time" + + osc_edit("/player/name", 's', &P1.name); + osc_edit("/player/health", 'i', &P1.health); + osc_edit("/player/time", 'f', &P1.time); + osc_edit("/player/visible", 'b', &P1.visible); + osc_edit_sync(-1, client_socket, 4); + + render_game(&P1); + } + } +} diff --git a/tools/editor/labs.osc/osclab1.c b/tools/editor/labs.osc/osclab1.c new file mode 100644 index 0000000..d4b76fb --- /dev/null +++ b/tools/editor/labs.osc/osclab1.c @@ -0,0 +1,74 @@ +#include "fwk.h" + +#define OSCPACK_C +#define OSCRECV_C +#define OSCSEND_C +#include "oscpack.h" +#include "oscrecv.h" +#include "oscsend.h" + +// networked gui, public api +void ui_netconfig(unsigned port); +void ui_netupdate(); + +// networked gui, private api +static int ui_netsocket = -1, ui_netclient, ui_netserver; +static map(char*,int) ui_netvalues_i; + +void ui_netconfig(unsigned port) { + do_once map_init(ui_netvalues_i, less_str, hash_str); + + if( ui_netsocket < 0 ) { + ui_netclient = !(tcp_bind("0.0.0.0",va("%d",port+1), 1) >= 0); + ui_netserver = !ui_netclient; + ui_netsocket = ui_netclient ? osc_open("127.0.0.1", va("%d",port)) : osc_listen("0.0.0.0", va("%d",port)); + if( ui_netsocket >= 0 ) ui_notify("UI Network config", va("Connected (%s).", ui_netclient ? "client (editor)" : "server (game)")); + if( ui_netsocket >= 0 ) window_title(ui_netclient ? "UI client (editor)" : "UI server (game)"); + if( ui_netsocket < 0 ) ui_netclient = ui_netserver = 0; + } +} +void ui_netupdate() { + if( ui_netserver ) { + // map_clear(ui_netvalues_i); + osc_update(ui_netsocket, 16); + } + if( ui_netclient ) { + for each_map_ptr(ui_netvalues_i, char *, name, int, value) { + char msg[4096]; + int msglen = osc_pack(msg, va("/%s", *name), "i", *value); + osc_send(ui_netsocket, msg + 4, msglen - 4); + } + // map_clear(ui_netvalues_i); + } +} + +int ui_netint(const char *name, int *value) { + const osc_message *found = osc_find(va("/%s", name)); // search in reverse order, so newest wins + if( found ) *value = (int)found->v[0].i; + + int changed = ui_int(name, value); + if( changed ) *map_find_or_add(ui_netvalues_i, (char*)name, *value) = *value; + + return changed; +} + +// demo +int main() { + window_create(0.66, WINDOW_SQUARE); + + ui_netconfig(1234); + + while( window_swap() ) { + + ui_netupdate(); + + static int r = 0, g = 0, b = 0; + if( ui_panel("test1", 0) ) { + ui_netint("Color R", &r); r = clampi(r, 0, 255); + ui_netint("Color G", &g); g = clampi(g, 0, 255); + ui_netint("Color B", &b); b = clampi(b, 0, 255); + ui_panel_end(); + } + viewport_color3(vec3(r/255.0,g/255.0,b/255.0)); + } +} diff --git a/tools/editor/labs.osc/osclab2.c b/tools/editor/labs.osc/osclab2.c new file mode 100644 index 0000000..0ebbe0c --- /dev/null +++ b/tools/editor/labs.osc/osclab2.c @@ -0,0 +1,106 @@ +// networked gui demo +// - rlyeh, public domain + +#include "fwk.h" + +#define OSCPACK_C +#define OSCRECV_C +#define OSCSEND_C +#include "oscpack.h" +#include "oscrecv.h" +#include "oscsend.h" + + + +int main() { + // window (80% sized, MSAA x2 flag) + window_create(80, WINDOW_MSAA2); + window_title(__FILE__); + + // scene loading + #define SCENE(...) #__VA_ARGS__ + const char *my_scene = SCENE([ + { + skybox: 'cubemaps/stardust/', + }, + { + position:[-5.0,-2.0,2.0], + rotation: [90.0,0.0,180.0], + scale:0.20, + mesh:'models/witch/witch.obj', + texture:'models/witch/witch_diffuse.tga.png', + flipuv:false, + }, + { + position:[-5.0,-2.0,2.0], + rotation: [90.0,0.0,180.0], + scale:0.20, + mesh:'models/witch/witch_object.obj', + texture:'models/witch/witch_object_diffuse.tga.png', + flipuv:false, + }, + ]); + int num_spawned = scene_merge(my_scene); + object_t *obj1 = scene_index(0); + object_t *obj2 = scene_index(1); + + // camera + camera_t cam = camera(); + cam.speed = 0.2f; + + // demo loop + while (window_swap()) + { + // input + if( input_down(KEY_ESC) ) break; + + // fps camera + bool active = ui_active() || ui_hover() || gizmo_active() ? false : input(MOUSE_L) || input(MOUSE_M) || input(MOUSE_R); + window_cursor( !active ); + + if( active ) cam.speed = clampf(cam.speed + input_diff(MOUSE_W) / 10, 0.05f, 5.0f); + vec2 mouse = scale2(vec2(input_diff(MOUSE_X), -input_diff(MOUSE_Y)), 0.2f * active); + vec3 wasdecq = scale3(vec3(input(KEY_D)-input(KEY_A),input(KEY_E)-(input(KEY_C)||input(KEY_Q)),input(KEY_W)-input(KEY_S)), cam.speed); + camera_move(&cam, wasdecq.x,wasdecq.y,wasdecq.z); + camera_fps(&cam, mouse.x,mouse.y); + + // queue model scale bounces + float t = fmod(window_time(), 0.3) / 0.3; + float s = 0.01f * ease_ping_pong(t, ease_in_cubic,ease_out_cubic); + object_scale(obj1, vec3(0.20f - s,0.20f + s,0.20f - s)); + object_scale(obj2, vec3(0.20f - s,0.20f + s,0.20f - s)); + + // flush render scene (background objects: skybox) + profile("Scene background") { + scene_render(SCENE_BACKGROUND); + } + + // queue debug drawcalls + profile("Debugdraw") { + ddraw_grid(0); + ddraw_color(YELLOW); + ddraw_text(vec3(+1,+1,-1), 0.04f, va("(%f,%f,%f)", cam.position.x,cam.position.y,cam.position.z)); + ddraw_color(YELLOW); + ddraw_flush(); + } + + // render scene (foreground objects) with post-effects + profile("Scene foreground") { + int scene_flags = 0; + scene_render(SCENE_FOREGROUND | scene_flags); + } + +#if 0 + do_once ui_netconfig(1234); + ui_netupdate(); + static int r = 0, g = 0, b = 0; + viewport_color3(vec3(r/255.0,g/255.0,b/255.0)); + if( ui_panel("net test1", 0) ) { + ui_netint("Color R", &r); r = clampi(r, 0, 255); + ui_netint("Color G", &g); g = clampi(g, 0, 255); + ui_netint("Color B", &b); b = clampi(b, 0, 255); + ui_panel_end(); + } +#endif + } +} diff --git a/tools/editor/labs.osc/oscpack.h b/tools/editor/labs.osc/oscpack.h new file mode 100644 index 0000000..a7fcf17 --- /dev/null +++ b/tools/editor/labs.osc/oscpack.h @@ -0,0 +1,162 @@ +// OSC buffer packing, +// - rlyeh, public domain. +// +// pack format: (i)nt, (h)int64, (t)ime, (f)loat, (s)tring, (S)ymbol, (c)har, (r)gba, (m)idi, +// (T)true, (N|F)nil+false, (I)nfinity, (b)lob, (d)ouble, @todo: ([)array. +// +// warning: osc_pack() generates OSC compliant messages, however, +// every osc_pack_va() call generates a 32-bit length prefix (in machine dependant order) + +API int osc_bundle( char *buf, uint64_t ts ); +API int osc_pack( char *buf, const char *addr, const char *fmt, ... ); +API char* osc_pack_va( const char *addr, const char *fmt, ... ); + + +#pragma once +/* +#ifdef _WIN32 +#include +#pragma comment(lib, "ws2_32") +#else +#include +#endif +*/ + +int osc__buffer_vl( char *buf, const char *fmt, va_list vl ) { + // if `buf` is NULL, just calc needed space + if( !buf ) { + int bytes = 0; + while( *fmt++ ) { + switch( fmt[-1] ) { + default: // bypass + break; case 'T': case 'F': case 'N': case 'I': bytes += 4; + break; case 'i': case 'c': case 'r': case 'm': bytes += 4; (void)va_arg(vl, uint32_t); + break; case 't': case 'h': bytes += 8; (void)va_arg(vl, uint64_t); + break; case 'd': bytes += 8; (void)va_arg(vl, double); + break; case 'f': bytes += 4; (void)va_arg(vl, double); + break; case 'b': bytes += va_arg(vl, uint32_t); (void)va_arg(vl, const char *); + break; case 's': case 'S': bytes += strlen( va_arg(vl, const char *) ) + 1; + } + } + return bytes; + } + + char *src = buf; + while( *fmt++ ) { + switch( fmt[-1] ) { + default: *buf++ = fmt[-1]; // bypass + break; case 'T': case 'F': case 'N': case 'I': + {} + break; case 'i': case 'c': case 'r': case 'm': + { uint32_t i = va_arg(vl, uint32_t); i = ntohl(i); memcpy(buf, &i, 4); buf += 4; } + break; case 't': case 'h': + { uint64_t l = va_arg(vl, uint64_t); l = ntohll(l); memcpy(buf, &l, 8); buf += 8; } + break; case 'd': + { union { double f; uint64_t i; } u; u.f = va_arg(vl, double); u.i = ntohll(u.i); memcpy(buf, &u.i, 8); buf += 8; } + break; case 'f': + { union { float f; uint32_t i; } u; u.f = (float)va_arg(vl, double); u.i = ntohl(u.i); memcpy(buf, &u.i, 4); buf += 4; } + break; case 'b': + { uint32_t l = va_arg(vl, uint32_t), ll = ntohl(l); memcpy(buf, &ll, 4); buf += 4; /*}*/ + /*{*/ const char *s = va_arg(vl, const char *); int32_t i = 0; + memcpy(buf, s, l); memcpy(buf + l, &i, 4); buf += l; buf += 3 - (((intptr_t)buf+3) & 3); } + break; case 's': case 'S': + { const char *s = va_arg(vl, const char *); int32_t i = 0, l = (int32_t)strlen(s) + 1; + memcpy(buf, s, l); memcpy(buf + l, &i, 4); buf += l; buf += 3 - (((intptr_t)buf+3) & 3); } + } + } + return buf - src; +} + +int osc__buffer( char *buf, const char *fmt, ... ) { + va_list vl; + va_start(vl, fmt); + int l = osc__buffer_vl(buf, fmt, vl); + va_end(vl); + return l; +} + +int osc_pack( char *buf, const char *addr, const char *fmt, ... ) { + char tmp[8192]; + va_list vl; + va_start(vl, fmt); + int l2 = osc__buffer_vl(tmp, fmt, vl); + va_end(vl); + int l1 = osc__buffer(buf, "s,s", addr, fmt); + memcpy(buf+l1, tmp, l2); + return l1+l2; +} + +char* osc_pack_va( const char *addr, const char *fmt, ... ) { // @todo: optimize me + char buf[1024]; + + char tmp[8192]; + va_list vl; + va_start(vl, fmt); + int l2 = osc__buffer_vl(tmp, fmt, vl); + va_end(vl); + int l1 = osc__buffer(buf, "s,s", addr, fmt); + memcpy(buf+l1, tmp, l2); + int total = l1+l2; + + char *out = va("%*.s", 4+total, ""); + memcpy(out, &total, 4); + memcpy(out+4, buf, total); + return out; +} + +int osc_bundle( char *buf, uint64_t ts ) { + return osc__buffer( buf, "sh", "#bundle", ts ); +} + +#ifdef OSCPACK_DEMO + +int main() { + // OSC message + { + char buf[4096]; + int l = osc_pack(buf, "/foo", "iisff", 1000, -1, "hello", 1.234f, 5.678f); + //use as: udp_send(socket, buf+4, l-4); + hexdump(buf, l); + + assert( 0 == memcmp( buf, + "\x2f\x66\x6f\x6f\x00\x00\x00\x00\x2c\x69\x69\x73\x66\x66\x00\x00" + "\x00\x00\x03\xe8\xff\xff\xff\xff\x68\x65\x6c\x6c\x6f\x00\x00\x00" + "\x3f\x9d\xf3\xb6\x40\xb5\xb2\x2d", l-4)); + } + + // OSC message (w/ initial 4-bytes payload) + { + char *buf = osc_pack_va("/foo", "iisff", 1000, -1, "hello", 1.234f, 5.678f); + int l = *(int*)buf; + printf("---------------%x\n", l); + hexdump(buf, l); + + assert( 0 == memcmp( buf+4, //"\x28\x00\x00\x00" + "\x2f\x66\x6f\x6f\x00\x00\x00\x00\x2c\x69\x69\x73\x66\x66\x00\x00" + "\x00\x00\x03\xe8\xff\xff\xff\xff\x68\x65\x6c\x6c\x6f\x00\x00\x00" + "\x3f\x9d\xf3\xb6\x40\xb5\xb2\x2d", l-4)); + } + + // OSC bundle + { + // OSC bundle test taken from Julien Pommier's oscpkt.hh + // wr.startBundle(); + // wr.addMessage("/foo").pushInt32(1000,-1).pushStr("hello").pushFloat(1.234f,5.678f); + // wr.endBundle(); + + char buf[4096]; + int h = osc_bundle(buf, 1ULL); + int m = osc_pack(buf+h, "/foo", "iisff", 1000, -1, "hello", 1.234f, 5.678f); + + int l = h+m; + hexdump(buf, h+m); + + assert( l == 0x3c-4 ); + assert( 0 == memcmp( buf, + "\x23\x62\x75\x6e\x64\x6c\x65\x00\x00\x00\x00\x00\x00\x00\x00\x01" + "\x2f\x66\x6f\x6f\x00\x00\x00\x00\x2c\x69\x69\x73" + "\x66\x66\x00\x00\x00\x00\x03\xe8\xff\xff\xff\xff\x68\x65\x6c\x6c" + "\x6f\x00\x00\x00\x3f\x9d\xf3\xb6\x40\xb5\xb2\x2d", l) ); + } +} +#endif diff --git a/tools/editor/labs.osc/oscrecv.h b/tools/editor/labs.osc/oscrecv.h new file mode 100644 index 0000000..08771b5 --- /dev/null +++ b/tools/editor/labs.osc/oscrecv.h @@ -0,0 +1,299 @@ +// simple osc server. designed for easy integration with a gameloop / immediate mode style. +// - rlyeh, public domain. forked from original code by @mmalex (public domain). +// +// @todo: add support for // wildcard +// @todo: add support for [ array parameters ] + +API int osc_listen( const char *mask, const char *port ); // creates a listening socket +API int osc_update(int s, int timeout_ms); // call every frame. reads the udp port and parse all messages found there +API int osc_list(const struct osc_message **first); // returns number of received messages, also set arg pointer to first item +API int osc_debug( FILE *out, const char *inmsg, int len ); // debugs raw osc buffer to stream +API const struct osc_message *osc_find(const char *addr); // finds most recent message matching 'addr' + +// OSC types from the spec. + +enum { + OSC_INT = 'i', + OSC_FLOAT = 'f', + OSC_STRING = 's', + OSC_SYMBOL = 'S', + OSC_BLOB = 'b', + OSC_INT64 = 'h', + OSC_TIME = 't', + OSC_DOUBLE = 'd', + OSC_CHAR = 'c', + OSC_RGBA = 'r', + OSC_MIDI = 'm', + OSC_TRUE = 'T', + OSC_FALSE = 'F', + OSC_NIL = 'N', + OSC_INFINITY = 'I', + // OSC_ARRAY = '[', // @todo +}; + +// OSC message + +typedef struct osc_message { + const char *pattern;// address in osc message + const char *types; // string of characters taken from the OSC types enum + const char *data; // pointer to raw osc data. for debugging purposes only (?) +#if 0 + int64_t i[8]; // integer interpretation of first 8 params (for blobs & strings, is length) + const char *s[8]; // for blobs and strings + float f[8]; // floating point interpretation of first 8 params +#else + union variant { + int64_t i; + const char *s; + double f; + uintptr_t up; + } v[8]; +#endif +} osc_message; + + +#pragma once + +enum { OSC_MAX_BUF = 8*1024*1024 }; // was: 65536 +enum { OSC_MAX_MESSAGES = 65536 }; // was: 1024 + +static int osc_match_(const char *pat, const char *addr) { + for (int n=0;*pat;addr++,pat++) switch (*pat) { + default: if (*pat!=*addr) return 0; break; + case '?': break; + case '[': n=(*++pat=='!'); for (pat+=n; *pat!=']' && *pat;++pat) { if (pat[1]=='-') { if (*addr>=*pat && *addr<=pat[2]) { n=!n; break; } pat+=pat[2] ? 2 : 1; } else if (*pat==*addr) { n=!n; break; } } + if (!n) return 0; while (*pat && *pat!=']') pat++; break; + case '{': n=0; for (const char *p=++pat; *p && *p!='}' && *p!='/'; pat=++p) { while (*p && *p!='}' && *p!='/' && *p!=',') p++; if (!strncmp(pat,addr,p-pat)) { addr+=p-pat; n=1; break; } } + while (*pat && *pat!='}') pat++; if (!n) return 0; break; + case '*': while (pat[1]=='*') pat++; n=0; if (pat[1]=='/' || pat[1]==0) { while (*addr && *addr!='/') ++addr; n=1; } else for (;*addr!='/' && *addr; ++addr) if (osc_match_(pat + 1, addr)) { n=1; break; } + if (!n) return 0; addr--; break; + // @todo: add // wildcard support + } + return *addr== 0; +} + +static float osc_asfloat_(uint32_t x) { union { float f; uint32_t i; } u; u.i=x; return u.f; } +static double osc_asdouble_(uint64_t x) { union { double f; uint64_t i; } u; u.i=x; return u.f; } + +static int osc_parse_i32_(const char **s, const char *e) { + if (*s+4>e) { *s=e; return 0; } + int rv=htonl(*(uint32_t*)*s); + *s+=4; + return rv; +} +static int64_t osc_parse_i64_(const char **s, const char *e) { + if (*s+8>e) { *s=e; return 0; } + int64_t rv=htonll(*(uint64_t*)*s); + *s+=8; + return rv; +} +static const char *osc_parse_str_(const char **s, const char *e) { + int len=(int)strlen(*s); + const char *rv=*s; + *s=rv+((len+4)&~3); + if (*s>e) *s=e; + return rv; +} +static const char *osc_parse_bin_(const char **s, const char *e, int *len) { + *len=osc_parse_i32_(s,e); + int maxlen=(int)(e-*s); + if (*len>maxlen) *len=maxlen; + if (*len<0) *len=0; + const char *rv=*s; + *s=rv+((*len+3)&~3); + if (*s>e) *s=e; + return rv; +} +static int osc_parse_(osc_message *out, int maxmsg, const char *s, const char *e) { + if (maxmsg<=0 || s>=e) return 0; + if (*(uint64_t*)s==*(uint64_t*)"#bundle\0") { // bundle is #bundle, uint64_t time, uint32_t length, + osc_parse_i64_(&s,e); // @todo: skipped time for now + int msgcount=0; + while (smaxlen) len=maxlen; + if (len<0) len=0; + int n=osc_parse_(out+msgcount, maxmsg-msgcount, s, s+len); + msgcount+=n; + s+=((len+3)&~3); + } + return msgcount; + } + // single message + memset(out,0,sizeof(osc_message)); + out->pattern=osc_parse_str_(&s,e); + if (!out->pattern) + return 0; + out->types=(*s==',')?osc_parse_str_(&s,e):",f"; + if (!out->types) + return 0; + out->types++; + out->data=s; + for (int param=0;param<8;++param) { + switch (out->types[param]) { + default: return 1; // done! + case OSC_CHAR: case OSC_RGBA: // all int32... + case OSC_MIDI: case OSC_INT: out->v[param].i=osc_parse_i32_(&s,e); break; + case OSC_TIME: case OSC_INT64: out->v[param].i=osc_parse_i64_(&s,e); break; + case OSC_STRING: case OSC_SYMBOL: out->v[param].s=osc_parse_str_(&s,e); /*out->v[param].i=strlen(out->v[param].s);*/ break; + case OSC_FLOAT: out->v[param].f=osc_asfloat_(osc_parse_i32_(&s,e)); break; + case OSC_DOUBLE: out->v[param].f=osc_asdouble_(osc_parse_i32_(&s,e)); break; + case OSC_BLOB: {int len=0; out->v[param].s=osc_parse_bin_(&s,e,&len); /*out->v[param].i=len;*/ break; } // @todo: important to signal len in variant somewhere + case OSC_INFINITY: out->v[param].f=INFINITY; break; + case OSC_TRUE: out->v[param].i=1; break; + case OSC_FALSE: case OSC_NIL: out->v[param].i=0; break; +// case OSC_ARRAY: @todo + } + } + return 1; +} + +static struct osc_message *msg = 0; //[OSC_MAX_MESSAGES]; +static int msgpos; + +int osc_listen( const char *mask, const char *port ) { + do_once udp_init(); + + int fd = swrapSocket(SWRAP_UDP,SWRAP_BIND,SWRAP_NOBLOCK,mask,port); + return fd; +} + +int osc_update(int fd, int timeout_ms /*= -1*/) { + do_once udp_init(); + + static char *buf = 0; // [OSC_MAX_BUF]; + static int bufpos; + + if( !buf ) { + buf = CALLOC( 1, OSC_MAX_BUF ); + msg = CALLOC( 1, OSC_MAX_MESSAGES * sizeof(struct osc_message) ); + } + if(fd<0) return 0; + + /* check if something is available */ + msgpos=0; + if( timeout_ms >= 0 ) { + int ret = swrapSelect(fd, timeout_ms / 1000.0); + if (ret <= 0) { // error, or timeout + return 0; + } + } + + for (msgpos=0,bufpos=0;msgpos0;) if (osc_match_(msg[i].pattern, addr)) return &msg[i]; + return 0; +} + +int osc_debug(FILE *fp, const char *buf, int len) { + osc_message m[16]; + int nn = osc_parse_(m, 16, buf, buf+len); + + for( int n = 0; n < nn; ++n ) { + fprintf(fp, "%s [%s]", m[n].pattern, m[n].types); + // @todo #bundle @%lld + + for(int i = 0; m[n].types[i]; ++i) { + char f = m[n].types[i]; + /**/ if (f == 'T' || f == 'F' || f == 'N') fprintf( fp, ",%s", (f=='T'?"True":f=='F'?"False":"Null")); + else if (f == 'h' || f == 't') fprintf( fp, ",%lld", m[n].v[i].i); + else if (f == 'f' || f == 'd') fprintf( fp, ",%f", m[n].v[i].f); + else if (f == 'i' || f == 'c' || f == 'r' || f == 'm' ) fprintf( fp, ",%d", (int)m[n].v[i].i); + else if (f == 's' || f == 'S' ) fprintf( fp, ",%s", m[n].v[i].s); + else if (f == 'b') fprintf( fp, ",%d bytes", (int)m[n].v[i].i); + else fprintf(fp, ",%s", "?"); + } + + fprintf(fp, "%s\n", ""); + } + return 1; +} + + +#ifdef OSCRECV_DEMO + +int main() { + // @mmalex's tests + assert( osc_match_("/[a-c]/?/[abc]/*/fish*/*food/f*/{foo,bar,baz}","/a/b/c//fishfood/monkeyfood/f/bar") ); + assert( !osc_match_("/[a-c]/?/[abc]/*/fish*/*food/f*/{fog,bar,baz}","/a/b/c//fishfood/monkeyfood/f/foo") ); + assert( !osc_match_("/[a-c]/?/[abc]/*/fith*/*food/f*/{foo,bar,baz}","/a/b/c//fishfood/monkeyfood/f/foo") ); + assert( !osc_match_("/[a-c]/?/[abc]/*/fish*/*good/f*/{foo,bar,baz}","/a/b/c//fishfood/monkeyfood/f/foo") ); + assert( !osc_match_("/[a-c]/?/[abc]/*/fish*/*food/g/{foo,bar,baz}","/a/b/c//fishfood/monkeyfood/f/foo") ); + assert( osc_match_("/[fa-cd]/?/[abc]/*/fish*/*food/f*/{foo,bar,baz}","/d/b/c//fishfood/monkeyfood/f/bar") ); + + // Julien Pommier's oscpkt tests + assert( !osc_match_("//bar", "bar")); +// assert( osc_match_("//bar", "/bar")); +// assert( osc_match_("//bar", "/foo/plop/bar")); +// assert( osc_match_("/foo//", "/foo/plop/df/")); +// assert( osc_match_("/foo///////bar", "/foo/plop/baz/bar")); + assert( osc_match_("*", "bar")); + assert( osc_match_("/foo/*", "/foo/bar")); +// assert( osc_match_("/{bar,fo}/b[aA]r", "/fo/bar")); +// assert( !osc_match_("/{bar,fo}/b[aA]r", "/foo/bar")); +// assert( osc_match_("/fo{bar,}/ba[e-t]", "/fo/bar")); + assert( !osc_match_("/fo{bar,}/ba[t-z]", "/fo/bar")); +// assert( osc_match_("/f{,ioio,bar}o/?a[!a]", "/fo/bar")); + assert( osc_match_("/foo/bar", "/foo/bar")); + assert( osc_match_("/f*o/bar", "/foo/bar")); + assert( osc_match_("/fo*o/bar", "/foo/bar")); +// assert( osc_match_("/*//bar", "/foo/bar")); + assert( osc_match_("/*/bar", "/foo/bar")); + assert( osc_match_("/*o/bar", "/foo/bar")); + assert( osc_match_("/*/*/*/*a***/*/*/*/*/", "/foo/bar/foo/barrrr/foo/bar/foo/barrrr/")); + assert( !osc_match_("/*/*/*/**/*/*/*/*/q", "/foo/bar/foo/barrrr/foo/bar/foo/barrrr/p")); + assert( osc_match_("[-]", "-")); +// assert( osc_match_("[a-]", "a")); +// assert( osc_match_("[a-]", "-")); + + int fd = osc_listen( "127.0.0.1", "9000" ); + if( fd ) for(;;) { + static unsigned char counter = 0; + printf("\r127.0.0.1:9000 %c", "\\|/-"[ counter = (counter+1) & 3 ]); + + Sleep(100); + osc_update(fd); + + const osc_message *begin; + for( int it = 0, end = osc_list(&begin); it < end; ++it ) { + const osc_message *msg = begin + it; + printf("> %s [%s]\n", msg->pattern, msg->types); + } + } +} + +#endif diff --git a/tools/editor/labs.osc/oscsend.c b/tools/editor/labs.osc/oscsend.c new file mode 100644 index 0000000..ede9bf6 --- /dev/null +++ b/tools/editor/labs.osc/oscsend.c @@ -0,0 +1,28 @@ +#include "fwk.h" +#include "oscsend.h" +#include "oscedit.h" +int main(int argc, char **argv) { + if( argc > 1 ) { + char *address = argv[1]; + char *arg = 0; strcatf(&arg, "%s", ""); for(int i = 2; i < argc; ++i) strcatf(&arg, "%s", argv[i]); + + char *message = 0; + if( !arg[0] ) message = osc_pack_va(address, ""); + else if( strchr("\'\"`", arg[0]) ) message = osc_pack_va(address, "s", (arg[strlen(arg)-1] = '\0', arg+1) ); + else if( !strcmp(arg, "true") || !strcmp(arg, "false") ) message = osc_pack_va(address, arg[0] == 't' ? "T" : "F"); + else if( strchr(".+-0123456789", arg[0]) && strpbrk(arg, ".fe") ) message = osc_pack_va(address, "f", atof(arg)); + else if( strchr( "+-0123456789", arg[0]) && strspn(arg+1, "0123456789") == strlen(arg)-1 ) message = osc_pack_va(address, "i", atoi(arg)); + else message = osc_pack_va(address, "s", arg ); + hexdump( message+4, *(int*)message ); + + int socket = osc_open("127.0.0.1", OSC_EDIT_PORT); + printf("%d bytes sent\n", osc_send( socket, message+4, *(int*)message )); + } else { + printf("%s /osc/address/ [argtype]\n", argv[0]); + printf("argtypes: [false][true][int][float][string]\n\n","\t"); + printf("example: %s /player/time 5.5\n", argv[0]); + printf("example: %s /player/health 100\n", argv[0]); + printf("example: %s /player/active true\n", argv[0]); + printf("example: %s /player/fullname \"john doe\"\n", argv[0]); + } +} diff --git a/tools/editor/labs.osc/oscsend.h b/tools/editor/labs.osc/oscsend.h new file mode 100644 index 0000000..7fcaeb8 --- /dev/null +++ b/tools/editor/labs.osc/oscsend.h @@ -0,0 +1,121 @@ +#pragma once +#define osc_open udp_open +#define osc_send udp_send +#define osc_close udp_close + +// rgb_formats: +// - 888 (RGB 3bytes/pixel), +// - 332 (RGB 1byte/pixel), +// - 242 (RGB 1byte/pixel), +// - 7755 (YCoCg 3bytes/2pixels) + +API void osc_send_fb( void *pixels, int w, int h, int comp, int rgb_format ); + + +// usage: +// void* rgb = render_capture(3); +// int rgbmode = 332; // 7755,242,332,888 safest+slowest +// osc_send_fb(rgb, window_width(), window_height(), 3, rgbmode); // 242 crash); 7755 green); + +//viewport_clear(false, true); +//ddraw_frame(NULL); +//ddraw_axis(10); + + +#pragma once +#include "oscpack.h" + +void osc_send_fb( void *pixels, int w, int h, int comp, int rgb_format ) { + static int s, *init = 0; + if( !init ) { init = &s; s = osc_open("127.0.0.1", "9000"); } + if( s <= 0 ) return; + + if( !pixels ) return; + if( (w * h * comp) <= 0 ) return; + + unsigned char* line_a = (unsigned char *)pixels; + unsigned char* line_b = (unsigned char *)pixels + (0 + (h - 1) * w) * comp; + unsigned char* line; + + static unsigned char activity = 0; + static unsigned num_sent_packets = 0; + + static char *oscbuf = 0; + if( !oscbuf ) oscbuf = (char*)MALLOC( 8192 * 4 + 64 ); // 8K RGBA32 max + some room for osc headers + + bool sending = 0; + int stride = w * comp; + + line = line_b + stride; + if( rgb_format == 888 ) for( int y = 0; y < h; ++y ) { line -= stride; + int osclen = osc_pack( oscbuf, "/render/", "iiiib", w,h,rgb_format, y, w*3,line); + bool sent = osc_send( s, oscbuf + 4, osclen - 4 ); + num_sent_packets += sent; sending |= sent; + } + if( rgb_format == 332 ) for( int y = 0; y < h; ++y ) { line -= stride; + for( int x = 0; x < w; ++x ) { + unsigned char r = line[x*3+0], g = line[x*3+1], b = line[x*3+2]; + line[x] = (( r >> 5 ) << 5) | (( g >> 5 ) << 2) | (( b >> 6 ) << 0); + } + int osclen = osc_pack( oscbuf, "/render/", "iiiib", w,h,rgb_format, y, w,line); + bool sent = osc_send( s, oscbuf + 4, osclen - 4 ); + num_sent_packets += sent; sending |= sent; + } + if( rgb_format == 242 ) for( int y = 0; y < h; ++y ) { line -= stride; + for( int x = 0; x < w; ++x ) { + unsigned char r = line[x*3+0], g = line[x*3+1], b = line[x*3+2]; + line[x] = (( r >> 6 ) << 6) | (( g >> 4 ) << 2) | (( b >> 6 ) << 0); + } + int osclen = osc_pack( oscbuf, "/render/", "iiiib", w,h,rgb_format, y, w,line); + bool sent = osc_send( s, oscbuf + 4, osclen - 4 ); + num_sent_packets += sent; sending |= sent; + } + if( rgb_format == 7755 ) for( int y = 0; y < h; ++y ) { line -= stride; + /**/ if( (w % 2) == 0 ) {} + else if( (w % 2) == 1 ) w -= 1; + assert( (w % 2) == 0 ); + uint8_t *out = line; + for( int x = 0, i = 0; i < w/2; ++i, ++x ) { + + // (/2>>1 /4>>2 /8>>3 /16>>4 /32>>5 /64>>6 /128>>7) + + // Y Y CoCg (7 7 5 5) : 3bytes == 2px !!wow!! + int32_t r,g,b,y0,y1,co0,co1,cg0,cg1; + r = line[x*6+0], g = line[x*6+1], b = line[x*6+2]; + y0 = r/4 + g/2 + b/4 + 1; // +1 to avoid some ycocg2rgb overflows + co0 = r/2 - b/2; + cg0 = g/2 - r/4 - b/4; + r = line[x*6+3], g = line[x*6+4], b = line[x*6+5]; + y1 = r/4 + g/2 + b/4 + 1; // +1 to avoid some ycocg2rgb overflows + co1 = r/2 - b/2; + cg1 = g/2 - r/4 - b/4; + int32_t co = (co0+co1)/2 + 128, cg = (cg0+cg1)/2 + 128; + // 7 7 5 5 + uint8_t y0_7 = y0 / 2, y1_7 = y1 / 2, co_5 = co / 8, cg_5 = cg / 8; + uint32_t pack24 = (y0_7<<17)|(y1_7<<10)|(co_5<<5)|(cg_5<<0); + *out++ = ( pack24 >> 16 ) & 255; + *out++ = ( pack24 >> 8 ) & 255; + *out++ = ( pack24 >> 0 ) & 255; + + // other packed options + // YCoCg (4 4 4) : 12bits == 1px + // YCoCg (2 3 3) : 1byte == 1px + // Y CoCg (8 4 4) : 2bytes == 1px + // Y Y CoCg (5 5 3 3) : 2bytes == 2px (meh) + // Y Y CoCg (8 8 4 4) : 3bytes == 2px + // Y Y CoCg (7 7 5 5) : 3bytes == 2px (wow!) + // Y Y Y CoCg (8 8 8 4 4) : 4bytes == 3px + // Y Y Y CoCg (7 7 7 5 6) : 4bytes == 3px + // Y Y Y CoCg (6 6 6 7 7) : 4bytes == 3px + // Y Y Y Y CoCg (7 7 7 7 6 6) : 5bytes == 4px + // Y Y Y Y CoCg (6 6 6 6 8 8) : 5bytes == 4px + } + int osclen = osc_pack( oscbuf, "/render/", "iiiib", w,h,rgb_format, y, w*2,line); + bool sent = osc_send( s, oscbuf + 4, osclen - 4 ); + num_sent_packets += sent; sending |= sent; + } + + activity += sending; + // printf("\r%c netsend %d", "\\|/-"[ activity % 4 ], num_sent_packets); + // stbi_write_png("out.png", w, h, 3, pixels, w * 3); +} diff --git a/tools/editor/labs.vm/ecs.c b/tools/editor/labs.vm/ecs.c new file mode 100644 index 0000000..e404065 --- /dev/null +++ b/tools/editor/labs.vm/ecs.c @@ -0,0 +1,425 @@ +// fast simple ecs +// - rlyeh, public domain +// +// features: +// - mostly heap allocation free +// - 2^64 systems max +// - 2^64 entities max +// +// cons: +// - 64 components max +// - memory requirements may be suboptimal. ie, using unions for the components: all components will equally size like the largest component. +// +// @note: +// - best perf when compiled with `/DNDEBUG /openmp /arch:AVX2 /Os /Ox /Gw /GL /MT` +// - runs 86M ops/sec on my old laptop: updates 50K entities out of 100K in 0.58ms/frame. 1000 frames in 0.57s +// +// @todo: +// - thread-safe +// - world/context management +// - dont use arrays: sequential access is killing performances when doing huge amount of entities (10M). worthy? +// compromise: use islands. 10K entities per island seems reasonable & best perf. the lower the better. +// we could parallelize islands as well (openmp? threads?). +// - file format spec: +// ; ecs data file format (.ini) +// [entity] +// components = mesh aabb +// position = 0 0 0 ; common +// rotation = 0 0 0 ; common +// scale = 2 2 2 ; common +// aabb.static = 1 +// aabb.size = 3 3 3 +// aabb.offset = 0 0 0 +// mesh.model = cube +// mesh.texture = wood_03 +// mesh.texture.tiling = 0.5 0.5 +// - file format parser: +// for each_map_sorted_ptr(ini("ecs.ini"), char *, k, char *, v) +// printf("'%s'='%s'\n", *k, *v += strspn(*v, " ")); +// + +/* api */ + +#ifdef _OPENMP +# ifdef _MSC_VER +# define parallel __pragma(omp parallel for) +# define has_parallel 1 +# else // __GNUC__ +# define parallel _Pragma("omp parallel for") // C99 +# define has_parallel 1 +# endif +#else +# define parallel +# define has_parallel 0 +#endif + +void ecs_max_components(int max_components); + +#define ecs_dump_world(fp, ...) ecs_dump_world(fp,ecs_mask64(__VA_ARGS__,-1)) + +#define ecs_add_entity(...) ecs_add_entity(ecs_mask64(__VA_ARGS__,-1)) + +union component_t* ecs_get_component( int eid, int cid ); +union component_t** ecs_get_components( int eid ); + +#define ecs_has_component(eid,...) ecs_has_component(eid,ecs_mask64(__VA_ARGS__,-1)) +#define ecs_add_component(eid,...) ecs_add_component(eid,ecs_mask64(__VA_ARGS__,-1)) +#define ecs_del_component(eid,...) ecs_del_component(eid,ecs_mask64(__VA_ARGS__,-1)) +#define ecs_use_component(eid,...) ecs_use_component(eid,ecs_mask64(__VA_ARGS__,-1)) +#define ecs_off_component(eid,...) ecs_off_component(eid,ecs_mask64(__VA_ARGS__,-1)) +#define each_ecs_component(obj, ...) \ + ( uint64_t sys_ = ecs_mask64(__VA_ARGS__,-1), ent_ = 0; ent_ < world.ne; ++ent_ ) \ + for( component_t **obj = sys_ == (sys_ & world.entities[ent_ * 2 + 0]) ? ecs_get_components(ent_) : 0; obj ; obj = 0 ) + +#if has_parallel +#undef each_ecs_component +#define each_ecs_component(obj, ...) \ + ( ent_ = (sys_ = ecs_mask64(__VA_ARGS__,-1), 0); ent_ < world.ne; ++ent_ ) \ + for( component_t **obj = sys_ == (sys_ & world.entities[ent_ * 2 + 0]) ? ecs_get_components(ent_) : 0; obj ; obj = 0 ) +static int64_t ent_; +static uint64_t sys_; +#endif + +/* internals */ + +typedef union component_t { + struct dummy { int dummy; }; + +#ifdef COMPONENT_HEADER +#include COMPONENT_HEADER +#endif +#ifdef COMPONENT_DATAS + COMPONENT_DATAS +#endif +#if defined ECS_DEMO || defined ECS_BENCH + struct position { float x,y,z; }; // c1 + struct velocity { float vx,vy,vz; }; // c2 + struct color { float r,g,b; }; // c3 + struct health { float health; }; // c4 + char *name; // c5 +#endif + +} component_t; + +struct world_t { + int ne, nc; // ne: number of entities, nc: number of components (stride) (cN) + array(uint64_t) entities; // vtable entities (2 entries/entity) [ e1(cflags,offset) e2(cflags,offset) .. eN(cflags,offset) ] + array(component_t) components; // instanced components (nc entries/entity) [ e1(c1,c2..) e2(c1,c2..) .. ] +}; + +uint64_t ecs_mask64( unsigned id1, ... ); +int (ecs_add_entity)( uint64_t component_mask ); +bool (ecs_has_component)( int eid, uint64_t flags ); +bool (ecs_add_component)( int eid, uint64_t flags ); +bool (ecs_del_component)( int eid, uint64_t flags ); +bool (ecs_use_component)( int eid, uint64_t flags ); +bool (ecs_off_component)( int eid, uint64_t flags ); +void (ecs_dump_world)( FILE *fp, uint64_t component_mask ); + +extern struct world_t world; + +// impl + +// static +struct world_t world = {0}; + +uint64_t ecs_mask64(unsigned id1, ... ) { + uint64_t flags = 0; + + // update flags and entities of components + va_list ap; + va_start(ap, id1); + for( uint64_t id = id1; id != ((unsigned)-1); id = va_arg(ap, unsigned) ) { + flags |= 1ull << id; + } + va_end(ap); + + return flags; +} + +int (ecs_add_entity)( uint64_t component_mask ) { + component_t c = {0}; + + // add mask+offset into entities + array_push(world.entities, component_mask); + array_push(world.entities, array_count(world.components)); + + // add components into entity + for (uint64_t cid = 0; cid < world.nc; ++cid) { + if( (1ull << cid) & component_mask ) { + array_push( world.components, c ); + } + } + + int eid = world.ne; + return world.ne++; +} + +void ecs_max_components(int max_comps) { + world.nc = max_comps; +} + +component_t* ecs_get_component( int eid, int target_cid ) { + uint64_t sys = world.entities[ eid * 2 + 0 ]; + uint64_t off = world.entities[ eid * 2 + 1 ]; + for( uint64_t cid = 0; cid < target_cid; ++cid ) { + off += !!((1ull << cid) & sys); + } + bool has_cid = !!((1ull << target_cid) & sys); + return (component_t*)(has_cid * (uintptr_t)(&world.components[ off ])); +} + +component_t** ecs_get_components( int eid ) { + static __thread component_t* local[8][64] = {0}; + static __thread int counter = 0; counter = (counter + 1) % 8; + + uint64_t sys = world.entities[ eid * 2 + 0 ]; + uint64_t off = world.entities[ eid * 2 + 1 ]; + for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid ) { + bool has_cid = !!((1ull << cid) & sys); + local[counter][ cid ] = (component_t*)(has_cid * (uintptr_t)(&world.components[ off ] + idx)); + idx += has_cid; + } + + return local[counter]; +} + +bool (ecs_has_component)( int eid, uint64_t flags ) { + uint64_t sys = world.entities[ eid * 2 + 0 ]; + return flags == (sys & flags); +} +bool (ecs_add_component)( int eid, uint64_t flags ) { + uint64_t sys = world.entities[ eid * 2 + 0 ]; + uint64_t off = world.entities[ eid * 2 + 1 ]; + + int eid2 = (ecs_add_entity)( sys | flags ); + uint64_t sys2 = world.entities[ eid2 * 2 + 0 ]; + uint64_t off2 = world.entities[ eid2 * 2 + 1 ]; + + for( uint64_t cid = 0, idx = 0, idx2 = 0; cid < world.nc; ++cid) { + if( (1ull << cid) & sys ) { + if( (1ull << cid) & sys2 ) { + memcpy( &world.components[ off2 ] + idx2, &world.components[ off ] + idx, sizeof(component_t)); + } + } + if( (1ull << cid) & sys ) ++idx; + if( (1ull << cid) & sys2 ) ++idx2; + } + + world.entities[ eid * 2 + 0 ] = sys2; + world.entities[ eid * 2 + 1 ] = off2; + + world.entities[ eid2 * 2 + 0 ] = 0; + world.entities[ eid2 * 2 + 1 ] = 0; + + return true; +} +bool (ecs_del_component)( int eid, uint64_t flags ) { // clr_component? + uint64_t sys = world.entities[ eid * 2 + 0 ]; + uint64_t off = world.entities[ eid * 2 + 1 ]; + for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid) { + if( (1ull << cid) & flags ) { + memset( &world.components[ off ] + idx, 0, sizeof(component_t)); + } + if( (1ull << cid) & sys ) ++idx; + } + world.entities[ eid * 2 + 0 ] &= ~flags; + return true; +} +bool (ecs_use_component)( int eid, uint64_t flags ) { + world.entities[ eid * 2 + 0 ] |= flags; + return true; +} +bool (ecs_off_component)( int eid, uint64_t flags ) { + world.entities[ eid * 2 + 0 ] &= ~flags; + return true; +} + +void (ecs_dump_world)( FILE *fp, uint64_t sys_mask ) { + if( sys_mask ) for( int eid = 0; eid < world.ne; ++eid ) { + uint64_t sys = world.entities[ eid * 2 + 0 ]; + uint64_t off = world.entities[ eid * 2 + 1 ]; + if( sys_mask != (sys & sys_mask)) continue; + fprintf(fp, "eid:%d sys:%#x ", eid, (unsigned)sys); + for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid ) { + if( (1ull << cid) & sys ) { + fprintf(fp, "cid:%d,%p+%d ", (int)cid, &world.components[ off ], (int)idx ); + ++idx; + } + } + fputc('\n', fp); + } +} + + +#ifdef ECS_BENCH +#include +#include + +int main(int argc, char **argv) { + int player; + + /* entities to spawn */ + #ifndef ECS_N + const int ECS_N = argc > 1 ? atoi(argv[1]) : 100000; + #endif + + /* frames to benchmark */ + #ifndef ECS_F + const int ECS_F = argc > 2 ? atoi(argv[2]) : 1000; + #endif + + // declare components: c1, c2, ... + enum { POSITION, VELOCITY, COLOR, HEALTH, INPUT, NAME, TOTAL }; + ecs_max_components(TOTAL); + + // spawn entities + { + double start = time_ss(); + + player = ecs_add_entity(NAME, POSITION, VELOCITY, HEALTH, INPUT); + ecs_get_component(player, VELOCITY)->vx = 1; + ecs_get_component(player, VELOCITY)->vy = 2; + + for (int i = 0; i < ECS_N; ++i) { + switch (i & 3) { + break; case 0:; /* static enemy */ + int enemy0 = ecs_add_entity(NAME, POSITION, COLOR, HEALTH); + break; case 1:; /* dynamic enemy */ + int enemy1 = ecs_add_entity(NAME, POSITION, COLOR, HEALTH, VELOCITY); + break; case 2:; /* static light */ + int light0 = ecs_add_entity(NAME, POSITION, COLOR); + break; case 3:; /* dynamic light */ + int light1 = ecs_add_entity(NAME, POSITION, COLOR, VELOCITY); + } + } + + double end = time_ss(); + double t = (end - start); + + int T = 1 * ECS_N; + printf("%17s: %d frame(s) * %d num_entities = %d total ops, in %.3fs => %.3fM ops/s, %.2fms/frame\n", + "spawn benchmark", 1, ECS_N, T, t, (T / 1000000.0) / t, (t * 1000 / 1) ); + } + + // process & benchmark + { + double start = time_ss(); + + for( int frame = 0; frame < ECS_F; ++frame ) { + parallel + for each_ecs_component(obj, POSITION, VELOCITY) { + component_t *p = obj[POSITION]; + component_t *v = obj[VELOCITY]; + p->x += v->vx; + p->y += v->vy; + p->z += v->vz; + } + } + + double end = time_ss(); + double t = (end - start); + + // stats + int num_iterated_entities = ECS_N; + int num_processed_entities = 0; + for each_ecs_component(obj, POSITION, VELOCITY) { + ++num_processed_entities; + } + + int T = ECS_F * num_processed_entities; + printf("%17s: %d frame(s) * %d num_entities = %d total ops, in %.3fs => %.3fM ops/s, %.2fms/frame\n", + "process benchmark", ECS_F, num_processed_entities, T, t, (T / 1000000.0) / t, (t * 1000 / ECS_F) ); + } + + {component_t *p = ecs_get_component(player, POSITION); + printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z );} + + assert( ecs_get_component(player, POSITION)->x == (ECS_F * 1)); + assert( ecs_get_component(player, POSITION)->y == (ECS_F * 2)); + + assert( ~puts("Ok") ); +} +#endif + +#ifdef ECS_DEMO +#include +#include + +int main(int argc, char **argv) { + // declare components: c1, c2, ... + enum { POSITION, VELOCITY, COLOR, HEALTH, INPUT, TOTAL }; + ecs_max_components(TOTAL); + + // spawn entities + int player = ecs_add_entity(POSITION, VELOCITY, HEALTH, INPUT); + ecs_get_component(player, VELOCITY)->vx = 1; + ecs_get_component(player, VELOCITY)->vy = 2; + + // some more + int enemy0, enemy1, light0, light1; + for (int i = 0; i < 10; ++i) { + if( 0 == (i&3)) /* static enemy */ enemy0 = ecs_add_entity(POSITION, COLOR, HEALTH); + if( 1 == (i&3)) /* dynamic enemy */ enemy1 = ecs_add_entity(POSITION, COLOR, HEALTH, VELOCITY); + if( 2 == (i&3)) /* static light */ light0 = ecs_add_entity(POSITION, COLOR); + if( 3 == (i&3)) /* dynamic light */ light1 = ecs_add_entity(POSITION, COLOR, VELOCITY), ecs_get_component(light1, VELOCITY)->vy = 2; + } + + { + component_t *p = ecs_get_component(player, POSITION); + printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z ); + p = ecs_get_component(player, VELOCITY); + printf("eid:%d (velocity: %f,%f)\n", player, p->vx, p->vy ); + + p = ecs_get_component(light1, POSITION); + printf("eid:%d (position: %f,%f,%f)\n", light1, p->x, p->y, p->z ); + p = ecs_get_component(light1, VELOCITY); + printf("eid:%d (velocity: %f,%f)\n", light1, p->vx, p->vy ); + } + + + // simulate system processing + int frames = 1000; + for( int frame = 0; frame < frames; ++frame ) { + for each_ecs_component(obj, POSITION, VELOCITY) { + component_t *p = obj[POSITION]; + component_t *v = obj[VELOCITY]; + p->x += v->vx; + p->y += v->vy; + p->z += v->vz; + } + } + + // verify (should display only player entity) + ecs_dump_world(stdout, INPUT); + + { + component_t *p = ecs_get_component(player, POSITION); + printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z ); + p = ecs_get_component(light1, POSITION); + printf("eid:%d (position: %f,%f,%f)\n", light1, p->x, p->y, p->z ); + } + + assert( ecs_get_component(player, POSITION)->x == (frames * 1)); + assert( ecs_get_component(player, POSITION)->y == (frames * 2)); + + assert( ecs_has_component(player, POSITION) ); + assert(!ecs_has_component(player, COLOR) ); + + assert( ecs_add_component(player, COLOR) ); + assert( ecs_has_component(player, COLOR) ); + + assert( ecs_off_component(player, POSITION) ); + assert(!ecs_has_component(player, POSITION) ); + assert( ecs_get_component(player, POSITION) == NULL); + assert( ecs_use_component(player, POSITION) ); + assert( ecs_has_component(player, POSITION) ); + assert( ecs_get_component(player, POSITION)->x == (frames * 1)); + + assert( ecs_del_component(player, POSITION) ); + assert(!ecs_has_component(player, POSITION) ); + assert( ecs_get_component(player, POSITION) == NULL ); + + assert( ~puts("Ok") ); +} +#endif diff --git a/tools/editor/labs.vm/hybrid-p2p.md b/tools/editor/labs.vm/hybrid-p2p.md new file mode 100644 index 0000000..761b5e6 --- /dev/null +++ b/tools/editor/labs.vm/hybrid-p2p.md @@ -0,0 +1,25 @@ +# Fair hybrid P2P model (rlyeh, public domain) + +## Abstract + +In gamedev, the problem with p2p is that decision-making falls to individuals, and they can unbalance the balance in their very own favor. For this reason, the client/server model is usually preferred, with the servers (and $maintenance$) being handled by the company that hosts the game. + +## Theory + +Following hybrid p2p system would be one in which a game is made up of many clusters of clients that play different games simultaneously, but in which the peers that act as referees actually belong to a different game, in such a way that a peer cannot Referee their own game. Basically if a peer is in a game-A with A-peers, it can only arbitrate a game-B with B-peers, all this while peer is also playing their game with other A peers. + +In addition, in order for peer A to arbitrate and initiate a game-B of B-peers, all nodes of B must send the source code of the server (with the rules of the game) to peer A for further validation. Once the source code is validated (sources must match for all the involved nodes), server-A will process the traffic of game-B, but note that server does not care about the rules of B, neither what the traffic of B looks like, and in fact, server-A does not know or care what peers in game-B are doing... what peer A does know as a player, however, is everything about game-A, whose refereers are in fact random external players to A set. + +## Appendix and notes + +``` +game A game B +[ player12 player16* player45 server33* player08 ] [ player49 player09 player33* server16* ] +``` + +- 16 is a player in A, and a refeerer in B. 33 is a player in B, and a refeerer in A. +- 33 agreed to arbitrate game A after checking data & code consensus for players { 12, 16, 45, 08 }. +- 16 agreed to arbitrate game B after checking data & code consensus for players { 49, 09, 33 }. +- traffic could be encrypted. public key could be a deterministic hash of all code+datas involved. +- peers could use an anonymous karma score system: positive for peers completing game sessions, negative for those peers cheating and/or with communication issues. + diff --git a/tools/editor/labs.vm/netproto.md b/tools/editor/labs.vm/netproto.md new file mode 100644 index 0000000..59554dd --- /dev/null +++ b/tools/editor/labs.vm/netproto.md @@ -0,0 +1,92 @@ +// minimalist network proto/scheme that resembles a .ini file. +// - rlyeh, public domain + +## features + +```json +types: + floating + integer + string + +signed: + yes/no + +sizes: + 1,8,12,15,16,24,32,64,... + +containers: + lists + maps (can be simulated by using two contiguous lists of keys and values) + +blocks: + enum + union (can be extended) + struct (can be extended) + +options: + zigzag, decay (double->half), packed, default, + +messaging: + numbered fields + optional + required + repeated + extensions +``` + +## binary format + +```ini +[cellsize in bytes:8] +[num key-values:cellsize] + [key id:cellsize][value id:cellsize] + [...] +[num types:cellsize] + [type class:cellsize][num items:cellsize][item1][item2][...] + [...] // would be nice to put strings at the very end, because of their length-variable nature +``` + +## text format + +```ini +// namespace +[[Demo]] + +// free-standing options +package = "tutorial" +version = 123 +pi = 3.14159 + +[PhoneType] // enumeration: all members are uppercase + no need for explicit values. +HOME +WORK +OTHER = HOME +MOBILE + +[PhoneNumber] // struct: variables never share same field number (1!=2) +1: number = 0 // .field 1, .id number, .value 0, .type Integer +2: phonetype = OTHER? // .field 2, .id phonetype, .value OTHER, .type PhoneType, .optional + +[Name] // union: some variables may share a very same field number (1==1) +1: name = "" // .field 1, .id name, .value "", .type String +1: nickname = "" // .field 1, .id nickname, .value "", .type String + +[Profile] // struct +1: name = Name... // .field 1, .id name, .type Name, .repeat +2: icon = "guest.png"? // .field 2, .id icon, .type String, .value = "guest.png", .optional + +[Contact] // struct +1: person = Profile +2: card_id = 0 bits:15 // .default = 0, .bits = 15 +3: email = ""? // email is an optional string located in the third message field that defaults to an empy string +4: phone = PhoneType... // .repeat +5: calls = 0... bits:64 packed // .default = 0, .repeat, .bits = 64, .packed +10..max: // user-defined, reserved extensions + +[ContactExt <- Contact] // struct that extends from Contact +1: phone_ext_number = 0? + +[AddressBook] // struct +1: contact = ContactExt... +``` diff --git a/tools/editor/plugins/.gitkeep b/tools/editor/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/editor/windows.ini b/tools/editor/windows.ini new file mode 100644 index 0000000..14f0d74 --- /dev/null +++ b/tools/editor/windows.ini @@ -0,0 +1,12 @@ +[Outliner] +x=0.000000 +y=0.045390 +w=0.250208 +h=0.953120 +visible=1 +[Properties] +x=0.780381 +y=0.045390 +w=0.219619 +h=0.953154 +visible=1