diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index de90a794..af8ff4e0 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -3,7 +3,7 @@ on: pull_request: types: [assigned, opened, synchronize, reopened] release: - types: [created] + types: [published] jobs: build-linux: @@ -110,7 +110,7 @@ jobs: main.wren release-artifacts: - if: ${{ github.event.action == 'created' }} + if: ${{ github.event.action == 'published' }} needs: [ build-linux, build-mac, build-windows ] runs-on: ubuntu-latest name: Upload Release Artifacts @@ -179,7 +179,7 @@ jobs: asset_content_type: application/zip notify-discord: runs-on: ubuntu-latest - if: ${{ github.event.action == 'created' }} + if: ${{ github.event.action == 'published' }} needs: [ release-artifacts ] steps: - name: Discord notification diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..e2e6a298 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,10 @@ +Authors +======= +We'd like to thank the following people for their contributions. + + * Aviv Beeri, aka springogeek [https://github.com/avivbeeri] + * scholar-mage [https://github.com/scholar-mage] + * Francisco Requena, aka frarees [https://github.com/frarees] + * Camilo Castro, aka clsource [https://github.com/clsource] + * Siddhant Rao [https://siddhantrao23.github.io] + * Chayim Refael Friedman [https://github.com/ChayimFriedman2] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b1ef428..f90cbd83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,6 @@ Minimalism: DOME is a minimal, but all-in-one toolkit. This means that it should Usage-First: Any new API is designed "usage-first", and then an implementation is built around that. This allows for a more "comfortable" and developer-friendly API, because it's how we would _want_ to use it. Once the API is designed, we do what we can to build an implementation to fit that design, adjusting when technical reasons require it. - ## Coding Conventions and Style Guides Code won't be merged into DOME if it doesn't match the existing code style. Here are some of the more obvious style decisions in the codebase. @@ -81,13 +80,3 @@ class GraphVisitor { ``` * Spaces around control-flow constructs, in similarity to C-styles. * Braces around control-flow blocks. - -## Contributors - -People who have contributed code or documentation to the project: -* Aviv Beeri, aka springogeek [https://github.com/avivbeeri] -* scholar-mage [https://github.com/scholar-mage] -* Francisco Requena, aka frarees [https://github.com/frarees] -* Camilo Castro, aka clsource [https://github.com/clsource] -* Siddhant Rao [https://siddhantrao23.github.io] -* Chayim Refael Friedman [https://github.com/ChayimFriedman2] diff --git a/Makefile b/Makefile index e15da5fe..01a47d39 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ SOURCE=src LIBS=lib OBJS=obj INCLUDES=include +SOURCE_FILES = $(shell find src -type f) UTILS = $(SOURCE)/util MODULES=$(SOURCE)/modules SCRIPTS=scripts @@ -80,11 +81,11 @@ WARNING_FLAGS += -Wno-incompatible-pointer-types-discards-qualifiers else ifneq ($(filter windows,$(TAGS)),) WARNING_FLAGS += -Wno-discarded-qualifiers -Wno-clobbered else ifneq ($(filter linux,$(TAGS)),) - WARNING_FLAGS += -Wno-clobbered + WARNING_FLAGS += -Wno-clobbered -Wno-maybe-uninitialized -Wno-attributes endif -CFLAGS = $(DOME_OPTS) -std=c99 -pedantic $(WARNING_FLAGS) +CFLAGS = $(DOME_OPTS) -std=c99 -pedantic $(WARNING_FLAGS) -fvisibility=hidden ifneq ($(filter macosx,$(TAGS)),) CFLAGS += -mmacosx-version-min=10.12 endif @@ -136,7 +137,7 @@ else ifneq ($(and $(filter macosx,$(TAGS)), $(filter framework,$(TAGS)), $(filte FFLAGS += -F/Library/Frameworks -framework SDL2 endif -LDFLAGS = -L$(LIBS) $(WINDOW_MODE_FLAG) $(SDLFLAGS) $(STATIC_FLAG) $(DEPS) +LDFLAGS = -L$(LIBS) $(WINDOW_MODE_FLAG) $(SDLFLAGS) $(STATIC_FLAG) $(DEPS) @@ -148,7 +149,7 @@ PROJECTS := dome.bin all: $(PROJECTS) WREN_LIB ?= $(LIBS)/libwren.a -WREN_PARAMS ?= $(ARCH) WREN_OPT_RANDOM=1 WREN_OPT_META=1 +WREN_PARAMS ?= $(ARCH) WREN_OPT_RANDOM=0 WREN_OPT_META=1 $(LIBS)/wren/lib/libwren.a: @echo "==== Cloning Wren ====" git submodule update --init -- $(LIBS)/wren @@ -166,14 +167,14 @@ $(OBJS)/vendor.o: $(INCLUDES)/vendor.c @echo "==== Building vendor module ====" $(CC) $(CFLAGS) -c $(INCLUDES)/vendor.c -o $(OBJS)/vendor.o $(IFLAGS) -$(OBJS)/main.o: $(SOURCE)/*.h $(SOURCE)/*.c $(MODULES)/*.c $(MODULES)/*.inc $(INCLUDES) $(WREN_LIB) +$(OBJS)/main.o: $(SOURCE_FILES) $(INCLUDES) $(WREN_LIB) $(MODULES)/*.inc @mkdir -p $(OBJS) @echo "==== Building core ($(TAGS)) module ====" $(CC) $(CFLAGS) -c $(SOURCE)/main.c -o $(OBJS)/main.o $(IFLAGS) $(TARGET_NAME): $(OBJS)/main.o $(OBJS)/vendor.o $(WREN_LIB) @echo "==== Linking DOME ($(TAGS)) ====" - $(CC) $(FFLAGS) -o $(TARGET_NAME) $(OBJS)/*.o $(ICON_OBJECT_FILE) $(LDFLAGS) + $(CC) $(CFLAGS) $(FFLAGS) -o $(TARGET_NAME) $(OBJS)/*.o $(ICON_OBJECT_FILE) $(LDFLAGS) ./scripts/set-executable-path.sh $(TARGET_NAME) @echo "DOME built as $(TARGET_NAME)" diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index b9d3ce42..58dcf225 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -13,7 +13,7 @@ -{% seo %} + {% seo %} @@ -35,73 +35,72 @@ -
-
- - {% if site.logo %} - Logo - {% endif %} +
+
+
-

{{ site.title | default: site.github.repository_name }}

+ {% if site.logo %} + Logo + {% endif %} -

{{ site.description | default: site.github.project_tagline }}

-

- - Download the latest version here! - -

+

{{ site.title | default: site.github.repository_name }}

- {% if site.github.is_project_page %} -

View the Project on GitHub {{ site.github.repository_nwo }}

- {% endif %} +

{{ site.description | default: site.github.project_tagline }}

+

+ + Download the latest version here! + +

-

- - - -

+ {% if site.github.is_project_page %} +

View the Project on GitHub {{ site.github.repository_nwo }}

+ {% endif %} -

- -

+ + +

+ - {% if site.github.is_user_page %} -

View My GitHub Profile

- {% endif %} - {% if site.show_downloads %} - - {% endif %} -
-
+ {% if site.github.is_user_page %} +

View My GitHub Profile

+ {% endif %} + + {% if site.show_downloads %} + + {% endif %} +
+
- {{ content }} + {{ content }} -
-
+ + + + {% if site.google_analytics %} + {% endif %} - - - - {% if site.google_analytics %} - - {% endif %} - - + + diff --git a/docs/assets/badge.svg b/docs/assets/badge.svg index f6dde21e..09df3116 100644 --- a/docs/assets/badge.svg +++ b/docs/assets/badge.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + background + + + + Layer 1 + + + + \ No newline at end of file diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index 9f7542a0..93b0a6d4 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -8,7 +8,7 @@ body { color: #ffffff; } -header h1 { +header { text-align: center; } @@ -26,9 +26,11 @@ h1, h2, h3, h4, h5, h6 { } .invert { - -webkit-filter: invert(100%); /* safari 6.0 - 9.0 */ - filter: invert(100%); - height: 53px; + height: 43px; + &:hover { + -webkit-filter: invert(15%); + filter: invert(15%); + } } .button { text-align: center; @@ -104,6 +106,14 @@ pre code { display: inline-block; } +a code { + text-decoration: underline; + + &:hover { + text-decoration: none; + } +} + .highlight, .highlight pre, .highlight table { @@ -177,3 +187,4 @@ pre code { .highlight .vg { color: #e270ef } /* Name.Variable.Global */ .highlight .vi { color: #e270ef } /* Name.Variable.Instance */ .highlight .il { color: #e270ef } /* Literal.Number.Integer.Long */ + diff --git a/docs/assets/dome-logo/dome-circle-flat.svg b/docs/assets/dome-logo/dome-circle-flat.svg index 346ca027..a04ca8c6 100644 --- a/docs/assets/dome-logo/dome-circle-flat.svg +++ b/docs/assets/dome-logo/dome-circle-flat.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + diff --git a/docs/index.md b/docs/index.md index b48fc089..cddadd96 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,9 +18,11 @@ Check out the documentation: * [io](modules/io) * [json](modules/json) * [math](modules/math) + * [plugin](modules/plugin) * Guides * [Import Resolution](guides/module-imports) * [Game Loop Behaviour](guides/game-loop) * [Distributing your game](guides/distribution) +* [Native Plugins](plugins/) * Examples * [DOMEjam](https://itch.io/jam/domejam) diff --git a/docs/modules/audio.md b/docs/modules/audio.md index ece575e1..eef7e3ad 100644 --- a/docs/modules/audio.md +++ b/docs/modules/audio.md @@ -13,9 +13,9 @@ It contains the following classes: ## AudioEngine -At the moment, DOME only supports OGG and WAV files, with a sample frequency of 44.1kHz (CD quality audio) +DOME supports playback of audio files in OGG and WAV formats. It will convert all files to its native sample rate of 44.1kHz (CD quality audio), but the re-sampling algorithm used is naive and may introduce audio artifacts. It is recommended that you produce your audio with a 44.1kHz sample-rate, for the best quality audio. -An audio file is loaded from disk into memory using the `load` function, and remains in memory until you call `unload(_)` or `unloadAll()`, or when DOME closes. +An audio file is loaded from disk into memory using the `load` function, and remains in memory until you call `unload(_)` or `unloadAll()`, or when DOME closes. If the audio needs to be resampled, this may take some time and block the main thread. When an audio file is about to be played, DOME allocates it an "audio channel", which handles the settings for volume, looping and panning. Once the audio is stopped or finishes playing, that channel is no longer usable, and a new one will need to be acquired. diff --git a/docs/modules/dome.md b/docs/modules/dome.md index 7c44a412..72aea753 100644 --- a/docs/modules/dome.md +++ b/docs/modules/dome.md @@ -6,10 +6,23 @@ The `dome` module allows you to control various aspects of how DOME as an applic It contains the following classes: +- [Platform](#platform) - [Process](#process) - [Version](#version) - [Window](#window) +## Platform + +Contains platform-specific utilities which may not always be supported. + +### Static Fields + +#### `name: String` +Returns the generic name of the operating system or platform, where possible. + +#### `time: Number` +Returns the integer number of seconds since the unix epoch (1st January 1970). + ## Process ### Static Fields @@ -84,6 +97,10 @@ Setting this to true will make the renderer wait for VSync before presenting to This is the width of the window/viewport, in pixels. +#### `static color: Color` + +This is the background color of the window. Note that to this _does not_ affect inside the drawing canvas: use `Canvas.cls()` to clear the canvas to some color. This field only affects the background color beyond canvas boundaries. Default is black. Cannot be transparent (transparency is ignored). + ### Static Methods #### `static resize(width: Number, height: Number): Void` diff --git a/docs/modules/index.md b/docs/modules/index.md index 401bde26..a49ad5ec 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -14,6 +14,7 @@ The modules you can import are here: * [io](io) * [json](json) * [math](math) +* [plugin](plugin) For example, the `graphics` module can be imported to access the `Canvas` and `Color` classes, like this: diff --git a/docs/modules/json.md b/docs/modules/json.md index a35498d2..d03dd799 100644 --- a/docs/modules/json.md +++ b/docs/modules/json.md @@ -95,6 +95,10 @@ This will encode solidus character (`/`). When converting a `Map` object to a `S By default _DOME_ aborts when there is a _JSON parsing error_ (triggers a `Fiber.abort()` on parse error). Turn off this option if you want to capture the _JsonError_ object. +#### `static checkCircular: Num` + +By default _DOME_ checks, when encoding, if your JSON contains circles, and aborts if it does. Turn off this option to not perform this check. This can speed up serializing, but will trigger an infinite recursion if the JSON indeed contains circles. + ### Example Use [Bitwise OR](https://wren.io/method-calls.html#operators) operator to select multiple options. diff --git a/docs/modules/plugin.md b/docs/modules/plugin.md new file mode 100644 index 00000000..fd5834c9 --- /dev/null +++ b/docs/modules/plugin.md @@ -0,0 +1,25 @@ +[< Back](.) + +plugin +================ + +The `plugin` module is the gateway to loading [native plugins](/plugins/) which can access lower level system features, or bind to other shared libraries. + +## Plugin + +### Static Methods + +#### `static load(name: String): Void` + +This will search the current execution context for a plugin of the given `name` and load it. + +The name of the library must be as follows, for different platforms. Assuming your plugin was named `"test"`: + * On Windows, the file would be `test.dll` + * On Mac OS X, the file must be named `test.dylib` + * On Linux and other platforms, the file must be `test.so` + +The `name` can be treated as a relative file path from the location of your application entry point, but it is recommended to place plugin library files in the same folder as your `main.wren` or `game.egg` file, for the best compatibility across platforms. + +Once the plugin library is loaded, DOME will execute its [Init hook](/plugins/#init), if available. If the hook reports a failure, or the library could not be found, `Plugin.load()` will abort the current fiber. + +A plugin is not unloaded until DOME shuts down. There is no way to unload it during execution. diff --git a/docs/modules/random.md b/docs/modules/random.md new file mode 100644 index 00000000..63b363f6 --- /dev/null +++ b/docs/modules/random.md @@ -0,0 +1,49 @@ +[< Back](.) + +random +============= + +The `random` module provides utilities for generating pseudo-random numbers, for a variety of applications. Please note, this module should not be used for applications which require a cryptographically secure source of random numbers. + +DOME's pseudo-random number generator is based on the "Squirrel3" noise function, described by [Squirrel Eiserloh](http://www.eiserloh.net/bio/) in [this talk](https://www.youtube.com/watch?v=LWFzPP8ZbdU). + +## Random + +### Static Methods + +#### `noise(x: Number): Number` +Given `x` as an integer, this will return a 32-bit number based on the Squirrel3 noise function. + +#### `noise(x: Number, seed: Number): Number` +Given `x` and `seed` as integers, this will return a 32-bit number based on the Squirrel3 noise function. The `seed` value can be used to get different outputs for the same position `x`. + +### Instance methods +#### `construct new()` +Creates a new instance of a random number generator, seeded based on the current system time. + +#### `construct new(seed: Number)` +Creates a new instance of a random number generator, based on the provided seed value. + +#### `float(): Number` +Returns a floating point value in the range of `0.0...1.0`, inclusive of `0.0` but exclusive of `1.0`. + +#### `float(end: Number): Number` +Returns a floating point value in the range of `0.0...end``, inclusive of `0.0` but exclusive of `end`. + +#### `float(start: Number, end: Number): Number` +Returns a floating point value in the range of `start...end``, inclusive of `start` but exclusive of `end`. + +#### `int(end: Number): Number` +Returns an integer in the range `0.0...end`, inclusive of `0.0` but exclusive of `end`.` +#### `int(start: Number, end: Number): Number` +Returns an integer in the range `start...end`, inclusive of `start` but exclusive of `end`.` + +#### `sample(list: List): Any` +Given a `list`, this will pick an element from that list at random. + +#### `sample(list: List, count: Number): List` +Randomly selects `count` elements from the list and returns them in a new list. This provides "sampling without replacement", so each element is distinct. + +#### `shuffle(list: List): List` +Uses the Fisher-Yates algorithm to shuffle the provided `list` in place. The list is also returned for convenience. + diff --git a/docs/plugins/example.md b/docs/plugins/example.md new file mode 100644 index 00000000..c0e7bff8 --- /dev/null +++ b/docs/plugins/example.md @@ -0,0 +1,141 @@ +[< Back](.) + +Plugin Example +============ +This example has two parts: A plugin file and a DOME application which loads it: + +## DOME application + +This is a basic example of loading a plugin, and making use of an external module +to access native methods. + +```c++ +import "plugin" for Plugin + +Plugin.load("test") +// The plugin will be initialised now + +// Plugins can register their own modules +import "external" for ExternalClass + + +class Game { + static init() { + // and allocators for foreign classes + var obj = ExternalClass.init() + + // and finally, they can register foreign methods implemented + // in the plugin native language. + obj.alert("Some words") + } + static update() {} + static draw(dt) {} +} +``` + + +## Plugin library + +A plugin can be written in any language that can compile down to a DLL, .so or .dylib file. This example will use C. + +```c +// Include some basic C definitions +#include + +// You'll need to include the DOME header +#include "dome.h" + +static DOME_API_v0* core; +static WREN_API_v0* wren; + +static const char* source = "" +"class ExternalClass {\n" // Source file for an external module + "construct init() {} \n" + "foreign alert(text) \n" +"} \n"; + +void allocate(WrenVM* vm) { + size_t CLASS_SIZE = 0; // This should be the size of your object's data + void* obj = wren->setSlotNewForeign(vm, 0, 0, CLASS_SIZE); +} + +void alertMethod(WrenVM* vm) { + // Fetch the method argument + const char* text = wren->getSlotString(vm, 1); + + // Retrieve the DOME Context from the VM. This is needed for many things. + DOME_Context ctx = core->getContext(vm); + + core->log(ctx, "%s\n", text); +} + +DOME_EXPORT DOME_Result PLUGIN_onInit(DOME_getAPIFunction DOME_getAPI, + DOME_Context ctx) { + + // Fetch the latest Core API and save it for later use. + core = DOME_getAPI(API_DOME, DOME_API_VERSION); + + // DOME also provides a subset of the Wren API for accessing slots + // in foreign methods. + wren = DOME_getAPI(API_WREN, WREN_API_VERSION); + + core->log(ctx, "Initialising external module\n"); + + // Register a module with it's associated source. + // Avoid giving the module a common name. + core->registerModule(ctx, "external", source); + + core->registerClass(ctx, "external", "ExternalClass", allocate, NULL); + core->registerFn(ctx, "external", "ExternalClass.alert(_)", alertMethod); + + // Returning anything other than SUCCESS here will result in the current fiber + // aborting. Use this to indicate if your plugin initialised successfully. + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_preUpdate(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_postUpdate(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} +DOME_EXPORT DOME_Result PLUGIN_preDraw(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} +DOME_EXPORT DOME_Result PLUGIN_postDraw(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_onShutdown(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} +``` + +## Compiling your plugin + +Compiling plugins is a little tricky, as each platform has slightly different requirements. +This section attempts to demonstrate ways of doing this on the common platforms. +Place the resulting compiled library file in the directory with your `main.wren` or `game.egg` and it should be detected by DOME. + +The `dome.h` file which provides the developer API is located in `../../include`, so we make sure this is added to the include folder list. +You can adjust this to suit your development environment as necessary. + +### Mac OS X +``` +> gcc -dynamiclib -o pluginName.dylib -I../../include plugin.c -undefined dynamic_lookup +``` + +We tell it we are building a `dynamiclib`, and that we want to treat `undefined` functions as fine using `dynamic_lookup`, so that methods can be acquired and linked at runtime. + +### Windows +``` +> gcc -O3 -std=gnu11 -shared -fPIC -I../../include pluginName.c -Wl,--unresolved-symbols=ignore-in-object-files -o pluginName.dll +``` +This example uses MSYS/MinGW for compiling the library on Windows, but there's no reason you couldn't use Visual Studio to compile a plugin instead. + +### Linux: +``` +> gcc -O3 -std=c11 -shared -o pluginName.so -fPIC -I../../include pluginName.c +``` + diff --git a/docs/plugins/index.md b/docs/plugins/index.md new file mode 100644 index 00000000..7fb3cbb9 --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,359 @@ +[< Back](..) + +Native Plugins +============ + + +Advanced developers are invited to build native plugins using a compiled language like C, C++ and Rust. This allows for deeper system access than DOME's module API's expose, as well as greater performance. It also makes the features of various shared libraries available, at a small cost. + +> Caution: The Native Plugin API is in an experimental phase and we may need to make some breaking changes in the future. Please keep an eye on this page for updates. + +# Contents + + * [Getting Started](#getting-started) + - [Example](#example) + - [Caveats](#caveats) + * Lifecycle hooks + - [Init](#init) + - [Pre-Update](#pre-update) + - [Post-Update](#post-update) + - [Pre-Draw](#pre-draw) + - [Post-Draw](#post-draw) + - [Shutdown](#shutdown) + * API Services + - [Core](#core) + * Enums + - [enum: DOME_Result](#enum-dome_result) + * Function Signatures + - [function: DOME_ForeignFn](#function-dome_foreignfn) + - [function: DOME_FinalizerFn](#function-dome_finalizerfn) + * Methods + - [method: registerModule](#method-registermodule) + - [method: registerClass](#method-registerclass) + - [method: registerFn](#method-registerfn) + - [method: lockModule](#method-lockmodule) + - [method: getContext](#method-getcontext) + - [method: log](#method-log) + - [Wren](#wren) + - [Audio](#audio) + * Enums + - [enum: CHANNEL_STATE](#enum-channel_state) + * Function Signatures + - [function: CHANNEL_mix](#function-channel_mix) + - [function: CHANNEL_callback](#function-channel_callback) + * Methods + - [method: channelCreate](#method-channelcreate) + - [method: getData](#method-getdata) + - [method: getState](#method-getstate) + - [method: setState](#method-setstate) + - [method: stop](#method-stop) + + +# Getting Started +In order to start writing your plugins, you will need to include `dome.h` in your project. This file can be found [here](https://github.com/domeengine/dome/blob/main/includes/dome.h) in the `includes` folder of the GitHub repository. +You will also need to configure your compiler/linker to ignore undefined methods and output a shared library. DOME supports plugins compiled as `.dll` (on Windows), `.so` (on Linux) and `.dylib` (on Mac OS X). + +The compiled library has to be available in the shared library path, which varies by operating system convention, however usually it can be placed in the same folder as your application entry point. You should consult your operating system's developer documentation for more details. + +You can load the plugin from your DOME application by calling [`Plugin.load(name)`](/modules/plugin) + +## Example + +You can find a well-commented example plugin and application on [this](example) page, which demonstrates all the currently available lifecycle hooks. + +## Caveats + +Using plugins with DOME can hugely expand the functions of your application, but there are certain things to be aware of: + + 1. Standard DOME applications are safely portable, as the engine is compiled for multiple platforms. This does not extend to plugins, which will need to be compiled for your target platforms and supplied with distributions of your application. + 2. DOME cannot verify the correctness of plugin implementations, which means that a plugin which has bugs could cause DOME to crash unexpectedly, or cause other issues with the underlying system. + 3. Your plugin will need to expose symbols with C-style function names. DOME cannot access functions whose names have been mangled. + +# Plugin Interfaces + +## Lifecycle hooks + +DOME can call specially named functions implemented by your plugin, at different times during the game loop. For this to work, you must ensure that your compiler does not mangle names. + +If a hook returns any result other than `DOME_RESULT_SUCCESS`, DOME will abort and shutdown. You should use the [`log(text)`](#method-log) call to print an error before returning. + +### Init + +``` +DOME_Result PLUGIN_onInit(DOME_getAPIFunction DOME_getAPI, + DOME_Context ctx) +``` + +DOME calls this function when the plugin is loaded, which gives you a chance to perform any initialisation you need to. +You can also signal to DOME that there was a problem by returning `DOME_RESULT_FAILURE`. + +This is also the best opportunity to acquire the available APIs, thanks to the `DOME_getAPI` function pointer, which is explained in the [API Services](#api-services) section. The structs returned from this call should be stored for use throughout the lifetime of your plugin. + +### Pre-Update +``` +DOME_Result PLUGIN_preUpdate(DOME_Context ctx) +``` +This hook is called before the Game.update step of the game loop. + +### Post-Update +``` +DOME_Result PLUGIN_postUpdate(DOME_Context ctx) +``` +This hook is called after the Game.update step of the game loop. + +### Pre-Draw +``` +DOME_Result PLUGIN_preDraw(DOME_Context ctx) +``` + +This hook is called before the Game.draw step of the game loop. + +### Post-Draw +``` +DOME_Result PLUGIN_postDraw(DOME_Context ctx) +``` +This hook is called after the Game.draw step of the game loop. + + +### Shutdown +``` +DOME_Result PLUGIN_onShutdown(DOME_Context ctx) +``` +This hook occurs when the plugin is being unloaded, usually because DOME is in the process of quitting. This is your last opportunity to free any resources your plugin is holding on to, and cleaning up any other background processes. + + + +# API Services + +The DOME Plugin API is split into different pieces, divided by general purpose and version. This is to allow maximum backwards-compatibility as new features are added. +The engine will endeavour to support previous versions of an API for as long as possible, but no guarentees will be made for compatibility across major versions of DOME. + +APIs are provided as a struct of function pointers, returned from: +``` +void* DOME_getAPI(API_TYPE type, int API_VERSION) +``` + +## Core + +This API allows your plugin to register modules and provides some basic utilities. + +### Acquisition + +``` +DOME_API_v0* core = (DOME_API_v0*)DOME_getAPI(API_DOME, DOME_API_VERSION); +``` + +### Enums: + +#### enum: DOME_Result + +Various methods return an enum of type `DOME_Result`, which indicates success or failure. These are the valid values: + + * `DOME_RESULT_SUCCESS` + * `DOME_RESULT_FAILURE` + * `DOME_RESULT_UNKNOWN` + +### Function signatures + +#### function: DOME_ForeignFn +`DOME_ForeignFn` methods have the signature: `void method(WrenVM* vm)` to match the `WrenForeignMethodFn` type. + +#### function: DOME_FinalizerFn +`DOME_FinalizerFn` methods have the signature: `void finalize(void* vm)`, to match the `WrenFinalizerFn` type. + +### Methods + +#### method: registerModule +``` +DOME_Result registerModule(DOME_Context ctx, + const char* name, + const char* moduleSource) +``` +This call registers module `name` with the source code `moduleSource`. You cannot register modules with the same name as DOME's internal modules. These are reserved. +Returns `DOME_RESULT_SUCCESS` if the module was successfully registered, and `DOME_RESULT_FAILURE` otherwise. + +#### method: registerClass +``` +DOME_Result registerClass(DOME_Context ctx, + const char* moduleName, + const char* className, + DOME_ForeignFn allocate, + DOME_FinalizerFn finalize) +``` +Register the `allocate` and `finalize` methods for `className` in `moduleName`, so that instances of the foreign class can be allocated, and optionally finalized. +The `finalize` method is your final chance to deal with the userdata attached to your foreign class. You won't have VM access inside this method. + +Returns `DOME_RESULT_SUCCESS` if the class is registered and `DOME_RESULT_FAILURE` otherwise. Failure will occur if `allocate` method is provided. The `finalize` argument can optionally be `NULL`. + + +#### method: registerFn +``` +DOME_Result registerFn(DOME_Context ctx, + const char* name, + const char* signature, + DOME_ForeignFn method) +``` +Register `method` as the function to call for the foreign method specified by `signature` in the module `name`. Returns `DOME_RESULT_SUCCESS` if the function was successfully registered, and `DOME_RESULT_FAILURE` otherwise. + +The format for the `signature` string is as follows: + * `static` if the method is a static class method, followed by a space, otherwise both are omitted. + * `ClassName` for the class method being declared, followed by a period (`.`) + * `methodName` which is the name of the field/method being exposed. + - If this is a field getter, nothing else is needed. + - If this is a field setter, add `=(_)` + - If this is a method, then parenthesis and a comma seperated list of underscores (`_`) follow, for the number of arguments the method takes. + - You can also use the setter and getter syntax for the class' subscript operator `[]`, which can be defined with one or more parameters. + - Wren methods can have up to 16 arguments, and are overloaded by arity. For example, `Test.do(_)` is considered different to `Test.do(_,_)` and so on. + +#### method: lockModule +``` +void lockModule(DOME_Context ctx, const char* name) +``` +This marks the module `name` as locked, so that further functions cannot modify it. It is recommended to do this after you have registered all the methods for your module, however there is no requirement to. + + + + +#### method: getContext +``` +DOME_Context getContext(WrenVM* vm) +``` +This allows foreign functions called by the Wren VM to access the current DOME context, to call various APIs. + +#### method: log +``` +void log(DOME_Context ctx, const char* text, ...) +``` + +Using this method allows for formatted output of `text` to the various debugging outputs DOME uses (stdout, a debug console on Windows and a DOME-log.txt file). + +You can use C-style specifiers for the `text` string, as used in the `printf` family of functions. + +## Wren + +You have access to a subset of the [Wren slot API](https://wren.io/embedding/slots-and-handles.html) in order to access parameters and return values in foreign methods. +The methods are incredibly well documented in the [Wren public header](https://github.com/wren-lang/wren/blob/main/src/include/wren.h), so we will not be documenting the functions here. + +You do not need to include the `wren.h` header in your application, as `dome.h` includes everything you need. + +### Acquisition + +``` +WREN_API_v0* wren = (WREN_API_v0*)DOME_getAPI(API_WREN, WREN_API_VERSION); +``` + +### Methods +This is a list of provided methods: +``` + void ensureSlots(WrenVM* vm, int slotCount); + void setSlotNull(WrenVM* vm, int slot); + void setSlotBool(WrenVM* vm, int slot, bool value); + void setSlotDouble(WrenVM* vm, int slot, double value); + void setSlotString(WrenVM* vm, int slot, const char* text); + void setSlotBytesWrenVM* vm, int slot, const char* data, size_t length); + void* setSlotNewForeign(WrenVM* vm, int slot, int classSlot, size_t length); + bool getSlotBool(WrenVM* vm, int slot); + double getSlotDouble(WrenVM* vm, int slot); +const char* getSlotString(WrenVM* vm, int slot); +const char* getSlotBytes(WrenVM* vm, int slot, int* length); + void abortFiber(WrenVM* vm, int slot); + + WrenType getSlotType(WrenVM* vm, int slot); + + int getListCount(WrenVM* vm, int slot); + void getListElement(WrenVM* vm, int listSlot, int index, int elementSlot); + void setListElement(WrenVM* vm, int listSlot, int index, int elementSlot); + void insertInList(WrenVM* vm, int listSlot, int index, int elementSlot); + + int getMapCount(WrenVM* vm, int slot); + bool getMapContainsKey(WrenVM* vm, int mapSlot, int keySlot); + void getMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot); + void setMapValue(WrenVM* vm, int mapSlot, int keySlot, int valueSlot); + void removeMapValue(WrenVM* vm, int mapSlot, int keySlot, int removedValueSlot); +``` + +## Audio + +This set of APIs gives you access to DOME's audio engine, to provide your own audio channel implementations. You can use this to synthesize sounds, or play custom audio formats. + +### Acquisition + +``` +AUDIO_API_v0* wren = (AUDIO_API_v0*)DOME_getAPI(API_AUDIO, AUDIO_API_VERSION); +``` + +### Enums + +#### enum: CHANNEL_STATE + +Audio channels are enabled and disabled based on a state, which is represented by this enum. Supported states are the following: + +``` +enum CHANNEL_STATE { + CHANNEL_INITIALIZE, + CHANNEL_TO_PLAY, + CHANNEL_PLAYING, + CHANNEL_STOPPING, + CHANNEL_STOPPED +} +``` + +### Function Signatures + +#### function: CHANNEL_mix +`CHANNEL_mix` functions have a signature of `void mix(CHANNEL_REF ref, float* buffer, size_t sampleRequestSize)`. + + * `ref` is a reference to the channel being mixed. + * `buffer` is an interleaved stereo buffer to write your audio data into. One sample is two values, for left and right, so `buffer` is `2 * sampleRequestSize` in size. + +#### function: CHANNEL_callback +`CHANNEL_callback` functions have this signature: `void callback(CHANNEL_REF ref, WrenVM* vm)`. + + +### Methods + +#### method: channelCreate +``` +CHANNEL_REF channelCreate(DOME_Context ctx, + CHANNEL_mix mix, + CHANNEL_callback update, + CHANNEL_callback finish, + void* userdata); +``` + +When you create a new audio channel, you must supply callbacks for mixing, updating and finalizing the channel. This allows it to play nicely within DOME's expected audio lifecycle. + +This method creates a channel with the specified callbacks and returns its corresponding CHANNEL_REF value, which can be used to manipulate the channel's state during execution. The channel will be created in the state `CHANNEL_INITIALIZE`, which gives you the opportunity to set up the channel configuration before it is played. + +The callbacks work like this: + - `update` is called once a frame, and can be used for safely modifying the state of the channel data. + - `finish` is called once the channel has been set to `STOPPED`, before its memory is released. + +The `userdata` is a pointer set by the plugin developer, which can be used to pass through associated data, and retrieved by [`getData(ref)`](#method-getdata). You are responsible for the management of the memory pointed to by that pointer. + + +#### method: getData +``` +void* getData(CHANNEL_REF ref) +``` +Fetch the `userdata` pointer for the given channel `ref`. + +#### method: getState +``` +CHANNEL_STATE getState(CHANNEL_REF ref) +``` +Get the current [state](#enum-channel_state) of the channel specified by `ref`. + +#### method: setState +``` +void setState(CHANNEL_REF ref, CHANNEL_STATE state) +``` +This allows you to specify the channel's [state](#enum-channel_state). DOME will only mix in channels in the following states: `CHANNEL_PLAYING` and `CHANNEL_STOPPING`. + +#### method: stop +``` +void stop(CHANNEL_REF ref) +``` +Marks the audio channel as having stopped. This means that DOME will no longer play this channel. It will call the `finish` callback at it's next opportunity. + + diff --git a/examples/alpha/main.wren b/examples/alpha/main.wren new file mode 100644 index 00000000..9742309d --- /dev/null +++ b/examples/alpha/main.wren @@ -0,0 +1,19 @@ +import "graphics" for Canvas, Color +var I = 255 +var D = -1 +class Game { + static init() {} + static update() { + I = I + D + if (I <= 0 || I >= 255) { + D = -D + } + } + static draw(dt) { + Canvas.cls() + Canvas.print("DOME Installed Successfully.", 10, 10, Color.white) + Canvas.print(I, 0, 0, Color.white) + Canvas.rectfill(10, 40, 50, 50, Color.rgb(0,255,0)) + Canvas.rectfill(10, 20, 50, 50, Color.rgb(255,0,0,I)) + } +} diff --git a/examples/audio/main.wren b/examples/audio/main.wren new file mode 100644 index 00000000..188c836e --- /dev/null +++ b/examples/audio/main.wren @@ -0,0 +1,53 @@ +import "graphics" for Canvas, Color +import "audio" for AudioEngine +import "input" for Keyboard +class Game { + static init() { + AudioEngine.load("music", "../spaceshooter/res/around-the-corner.ogg") + AudioEngine.load("sfx", "../spaceshooter/res/Laser_Shoot.wav") + __channel = AudioEngine.play("music") + + } + static update() { + if (Keyboard["return"].justPressed) { + AudioEngine.play("sfx") + } + if (Keyboard["space"].justPressed) { + __channel = AudioEngine.play("music") + } + if (Keyboard["backspace"].justPressed) { + __channel.stop() + } + if (Keyboard["0"].justPressed) { + AudioEngine.stopAllChannels() + } + if (Keyboard["-"].justPressed) { + AudioEngine.unload("music") + AudioEngine.unload("sfx") + } + if (Keyboard["left"].down) { + __channel.pan = __channel.pan - 0.01 + } + if (Keyboard["right"].down) { + __channel.pan = __channel.pan + 0.01 + } + if (Keyboard["down"].down) { + __channel.volume = __channel.volume - 0.01 + } + if (Keyboard["up"].down) { + __channel.volume = __channel.volume + 0.01 + } + if (__channel.pan.abs < 0.009) { + __channel.pan = 0 + } + if (__channel.volume.abs < 0.009) { + __channel.volume = 0 + } + } + static draw(dt) { + Canvas.cls() + Canvas.print(__channel == null ? "Nothing Playing" : "Music Playing", 10, 10, Color.white) + Canvas.print(__channel == null ? "" : "Volume %(__channel.volume)", 10, 18, Color.white) + Canvas.print(__channel == null ? "" : "Pan %(__channel.pan)", 10, 26, Color.white) + } +} diff --git a/examples/plugin/Makefile b/examples/plugin/Makefile new file mode 100644 index 00000000..2ccbf79f --- /dev/null +++ b/examples/plugin/Makefile @@ -0,0 +1,9 @@ +.PHONY: test.dylib +test.dylib: test.c + gcc -dynamiclib -o test.dylib -I../../include test.c -undefined dynamic_lookup + +test.so: test.c + gcc -O3 -std=c11 -shared -o test.so -fPIC -I../../include test.c + +test.dll: test.c + gcc -O3 -std=gnu11 -shared -fPIC -I../../include test.c -Wl,--unresolved-symbols=ignore-in-object-files -o test.dll diff --git a/examples/plugin/main.wren b/examples/plugin/main.wren new file mode 100644 index 00000000..47a8f422 --- /dev/null +++ b/examples/plugin/main.wren @@ -0,0 +1,21 @@ +import "plugin" for Plugin + +Plugin.load("test") +// The plugin will be initialised now + +// Plugins can register their own modules +import "external" for ExternalClass + + +class Game { + static init() { + // and allocators for foreign classes + var obj = ExternalClass.init() + + // and finally, they can register foreign methods implemented + // in the plugin native language. + obj.alert("Some words") + } + static update() {} + static draw(dt) {} +} diff --git a/examples/plugin/test.c b/examples/plugin/test.c new file mode 100644 index 00000000..5da72666 --- /dev/null +++ b/examples/plugin/test.c @@ -0,0 +1,70 @@ +#include +// You'll need to include the DOME header +#include "dome.h" + +static DOME_API_v0* core; +static WREN_API_v0* wren; + +static const char* source = "" +"class ExternalClass {\n" // Source file for an external module + "construct init() {} \n" + "foreign alert(text) \n" +"} \n"; + +void allocate(WrenVM* vm) { + size_t CLASS_SIZE = 0; // This should be the size of your object's data + void* obj = wren->setSlotNewForeign(vm, 0, 0, CLASS_SIZE); +} + +void alertMethod(WrenVM* vm) { + // Fetch the method argument + const char* text = wren->getSlotString(vm, 1); + + // Retrieve the DOME Context from the VM. This is needed for many things. + DOME_Context ctx = core->getContext(vm); + + core->log(ctx, "%s\n", text); +} + +DOME_EXPORT DOME_Result PLUGIN_onInit(DOME_getAPIFunction DOME_getAPI, + DOME_Context ctx) { + + // Fetch the latest Core API and save it for later use. + core = DOME_getAPI(API_DOME, DOME_API_VERSION); + + // DOME also provides a subset of the Wren API for accessing slots + // in foreign methods. + wren = DOME_getAPI(API_WREN, WREN_API_VERSION); + + core->log(ctx, "Initialising external module\n"); + + // Register a module with it's associated source. + // Avoid giving the module a common name. + core->registerModule(ctx, "external", source); + + core->registerClass(ctx, "external", "ExternalClass", allocate, NULL); + core->registerFn(ctx, "external", "ExternalClass.alert(_)", alertMethod); + + // Returning anything other than SUCCESS here will result in the current fiber + // aborting. Use this to indicate if your plugin initialised successfully. + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_preUpdate(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_postUpdate(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} +DOME_EXPORT DOME_Result PLUGIN_preDraw(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} +DOME_EXPORT DOME_Result PLUGIN_postDraw(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + +DOME_EXPORT DOME_Result PLUGIN_onShutdown(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + diff --git a/examples/plugin/test.dylib b/examples/plugin/test.dylib new file mode 100755 index 00000000..05058ffb Binary files /dev/null and b/examples/plugin/test.dylib differ diff --git a/examples/random/main.wren b/examples/random/main.wren new file mode 100644 index 00000000..10d49f40 --- /dev/null +++ b/examples/random/main.wren @@ -0,0 +1,25 @@ +import "random" for Random +import "dome" for Process + +class Game { + static init() { + for (n in 0...100) { + var start = System.clock + var RNG = Random.new() + var n = 0 + + for (i in 0...1000000) { + n = n + RNG.float() + } + n = n / 1000000 + + var end = System.clock + System.print("Time: %(end - start) seconds") + } + Process.exit() + } + static update() {} + static draw(d) {} + + +} diff --git a/include/dome.h b/include/dome.h new file mode 100644 index 00000000..f67f8643 --- /dev/null +++ b/include/dome.h @@ -0,0 +1,213 @@ +/* DOME Plugin Header v0.0.1 */ + +#include +#include +#ifndef DOME_PLUGIN_H +#define DOME_PLUGIN_H + +// Define external for any platform +#if defined _WIN32 || defined __CYGWIN__ + // Exporting... + #ifdef __GNUC__ + #define DOME_EXPORT __attribute__ ((dllexport)) + #else + #define DOME_EXPORT __declspec(dllexport) // Note: actually gcc seems to also supports this syntax. + #endif + #ifdef __GNUC__ + #define DOME_IMPORT __attribute__ ((dllimport)) + #else + #define DOME_IMPORT __declspec(dllimport) // Note: actually gcc seems to also supports this syntax. + #endif + #define DOME_INTERNAL +#else + #if __GNUC__ >= 4 + #define DOME_EXPORT __attribute__ ((visibility ("default"))) + #define DOME_INTERNAL __attribute__ ((visibility ("hidden"))) + #else + #define DOME_EXPORT + #define DOME_INTERNAL + #endif + #define DOME_IMPORT +#endif + +#ifdef WIN_EXPORT + #define PUBLIC_EXPORT DOME_EXPORT +#else + #define PUBLIC_EXPORT DOME_IMPORT +#endif + + +typedef enum { + API_DOME, + API_WREN, + API_AUDIO +} API_TYPE; + +#define DOME_API_VERSION 0 +#define WREN_API_VERSION 0 +#define AUDIO_API_VERSION 0 + +// Opaque context pointer +typedef void* DOME_Context; + +typedef enum { + DOME_RESULT_SUCCESS, + DOME_RESULT_FAILURE, + DOME_RESULT_UNKNOWN +} DOME_Result; + +#ifndef wren_h +// If the wren header is not in use, we forward declare some types we need. +typedef struct WrenVM WrenVM; +typedef void (*WrenForeignMethodFn)(WrenVM* vm); +typedef void (*WrenFinalizerFn)(void* data); + +typedef enum +{ + WREN_TYPE_BOOL, + WREN_TYPE_NUM, + WREN_TYPE_FOREIGN, + WREN_TYPE_LIST, + WREN_TYPE_MAP, + WREN_TYPE_NULL, + WREN_TYPE_STRING, + + // The object is of a type that isn't accessible by the C API. + WREN_TYPE_UNKNOWN +} WrenType; +#endif + +typedef DOME_Result (*DOME_Plugin_Hook) (DOME_Context context); +typedef WrenForeignMethodFn DOME_ForeignFn; +typedef WrenFinalizerFn DOME_FinalizerFn; + + + + +// DO NOT CHANGE ORDER OF THESE STRUCTS, to preserve ABI. +typedef struct { + const char* name; + DOME_Plugin_Hook preUpdate; + DOME_Plugin_Hook postUpdate; + DOME_Plugin_Hook preDraw; + DOME_Plugin_Hook postDraw; + DOME_Plugin_Hook onShutdown; +} PLUGIN; + +typedef struct { + void (*ensureSlots)(WrenVM* vm, int slotCount); + + void (*setSlotNull)(WrenVM* vm, int slot); + void (*setSlotBool)(WrenVM* vm, int slot, bool value); + void (*setSlotDouble)(WrenVM* vm, int slot, double value); + void (*setSlotString)(WrenVM* vm, int slot, const char* text); + void (*setSlotBytes)(WrenVM* vm, int slot, const char* data, size_t length); + void* (*setSlotNewForeign)(WrenVM* vm, int slot, int classSlot, size_t length); + void (*setSlotNewList)(WrenVM* vm, int slot); + void (*setSlotNewMap)(WrenVM* vm, int slot); + + DOME_Context (*getUserData)(WrenVM* vm); + bool (*getSlotBool)(WrenVM* vm, int slot); + double (*getSlotDouble)(WrenVM* vm, int slot); + const char* (*getSlotString)(WrenVM* vm, int slot); + const char* (*getSlotBytes)(WrenVM* vm, int slot, int* length); + void* (*getSlotForeign)(WrenVM* vm, int slot); + + void (*abortFiber)(WrenVM* vm, int slot); + int (*getSlotCount)(WrenVM* vm); + WrenType (*getSlotType)(WrenVM* vm, int slot); + + int (*getListCount)(WrenVM* vm, int slot); + void (*getListElement)(WrenVM* vm, int listSlot, int index, int elementSlot); + void (*setListElement)(WrenVM* vm, int listSlot, int index, int elementSlot); + void (*insertInList)(WrenVM* vm, int listSlot, int index, int elementSlot); + + int (*getMapCount)(WrenVM* vm, int slot); + bool (*getMapContainsKey)(WrenVM* vm, int mapSlot, int keySlot); + void (*getMapValue)(WrenVM* vm, int mapSlot, int keySlot, int valueSlot); + void (*setMapValue)(WrenVM* vm, int mapSlot, int keySlot, int valueSlot); + void (*removeMapValue)(WrenVM* vm, int mapSlot, int keySlot, int removedValueSlot); +} WREN_API_v0; + +typedef struct { + DOME_Result (*registerModule)(DOME_Context ctx, const char* name, const char* source); + DOME_Result (*registerFn)(DOME_Context ctx, const char* name, const char* signature, DOME_ForeignFn method); + DOME_Result (*registerClass)(DOME_Context ctx, const char* moduleName, const char* className, DOME_ForeignFn allocate, DOME_FinalizerFn finalize); + void (*lockModule)(DOME_Context ctx, const char* name); + DOME_Context (*getContext)(WrenVM* vm); + void (*log)(DOME_Context ctx, const char* text, ...); +} DOME_API_v0; + +typedef uint64_t CHANNEL_ID; +typedef struct { + CHANNEL_ID id; + void* engine; +} CHANNEL_REF; + +typedef struct CHANNEL_t CHANNEL; +typedef enum { + CHANNEL_INVALID, + CHANNEL_INITIALIZE, + CHANNEL_TO_PLAY, + CHANNEL_DEVIRTUALIZE, + CHANNEL_LOADING, + CHANNEL_PLAYING, + CHANNEL_STOPPING, + CHANNEL_STOPPED, + CHANNEL_VIRTUALIZING, + CHANNEL_VIRTUAL, + CHANNEL_LAST +} CHANNEL_STATE; + +typedef void (*CHANNEL_mix)(CHANNEL_REF ref, float* buffer, size_t requestedSamples); +typedef void (*CHANNEL_callback)(CHANNEL_REF ref, WrenVM* vm); + +typedef struct { + CHANNEL_REF (*channelCreate)(DOME_Context ctx, CHANNEL_mix mix, CHANNEL_callback update, CHANNEL_callback finish, void* userdata); + CHANNEL_STATE (*getState)(CHANNEL_REF ref); + void (*setState)(CHANNEL_REF ref, CHANNEL_STATE state); + void (*stop)(CHANNEL_REF ref); + void* (*getData)(CHANNEL_REF ref); +} AUDIO_API_v0; + +typedef void* (*DOME_getAPIFunction)(API_TYPE api, int version); +PUBLIC_EXPORT void* DOME_getAPI(API_TYPE api, int version); + + +// Helper macros to abstract the api->method + +#define DOME_registerModule(ctx, name, src) api->registerModule(ctx, name, src) +#define DOME_registerClass(ctx, module, className, allocate, finalize) api->registerClass(ctx, module, className, allocate, finalize) +#define DOME_registerFn(ctx, module, signature, method) \ + api->registerFn(ctx, module, signature, PLUGIN_method_wrap_##method) +#define DOME_lockModule(ctx, module) api->lockModule(ctx, module) +#define DOME_getContext(vm) (api->getContext(vm)) + +#define DOME_log(ctx, ...) (api->log(ctx, ##__VA_ARGS__)) + +#define PLUGIN_method(name, ctx, vm) \ + static void PLUGIN_method_##name(DOME_Context ctx, WrenVM* vm); \ + DOME_EXPORT void PLUGIN_method_wrap_##name(WrenVM* vm) { \ + DOME_Context ctx = DOME_getContext(vm); \ + PLUGIN_method_##name(ctx, vm);\ + } \ + static void PLUGIN_method_##name(DOME_Context ctx, WrenVM* vm) + +#define GET_BOOL(slot) wren->getSlotBool(vm, slot) +#define GET_NUMBER(slot) wren->getSlotDouble(vm, slot) +#define GET_STRING(slot) wren->getSlotString(vm, slot) +#define GET_BYTES(slot, length) wren->getSlotBytes(vm, slot, length) +#define GET_FOREIGN(slot) wren->getSlotForeign(vm, slot) +#define RETURN_NULL() wren->setSlotNull(vm, 0); +#define RETURN_NUMBER(value) wren->setSlotDouble(vm, 0, value); +#define RETURN_BOOL(value) wren->setSlotBool(vm, 0, value); +#define RETURN_STRING(value) wren->setSlotString(vm, 0, value); +#define RETURN_BYTES(value, length) wren->setSlotBytes(vm, 0, value, length); + +#define THROW_ERROR(message) \ + do { \ + wren->setSlotString(vm, 0, message); \ + wren->abortFiber(vm, 0); \ + } while (false) + +#endif diff --git a/include/mkdirp/mkdirp.c b/include/mkdirp/mkdirp.c index 615e5d66..0e23e0fc 100644 --- a/include/mkdirp/mkdirp.c +++ b/include/mkdirp/mkdirp.c @@ -36,7 +36,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #define PATH_SEPARATOR '/' #endif -char * +const char* path_normalize(const char *path) { if (!path) return NULL; diff --git a/include/mkdirp/mkdirp.h b/include/mkdirp/mkdirp.h index 8f8d361e..63a50db8 100644 --- a/include/mkdirp/mkdirp.h +++ b/include/mkdirp/mkdirp.h @@ -34,5 +34,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ int mkdirp(const char *, mode_t ); +const char* path_normalize(const char* path); #endif diff --git a/scripts/generateEmbedModules.sh b/scripts/generateEmbedModules.sh index d5919a8a..0ef02220 100755 --- a/scripts/generateEmbedModules.sh +++ b/scripts/generateEmbedModules.sh @@ -4,8 +4,9 @@ cd src/util gcc embed.c -o embed -std=c99 declare -a arr=( -"dome" -"input" +"stringUtils" +"dome" +"input" "graphics" "color" "font" @@ -14,7 +15,10 @@ declare -a arr=( "vector" "image" "math" +"plugin" "json" +"platform" +"random" ) declare -a opts=( diff --git a/scripts/setup_static_linux_sdl.sh b/scripts/setup_static_linux_sdl.sh index 3f4a94bb..e1c1d02b 100755 --- a/scripts/setup_static_linux_sdl.sh +++ b/scripts/setup_static_linux_sdl.sh @@ -17,14 +17,21 @@ fi make +# These two are essential +cp $DIRECTORY/${FOLDER}/build/.libs/libSDL2.a $LIB_DIR +cp $DIRECTORY/${FOLDER}/sdl2-config $LIB_DIR/sdl2-config + if [ -f "$DIRECTORY/$FOLDER/build/.libs/libSDL2main.a" ]; then cp $DIRECTORY/${FOLDER}/build/.libs/libSDL2main.a $LIB_DIR +fi +if [ -f "$DIRECTORY/$FOLDER/build/.libs/libSDL2main.so" ]; then cp $DIRECTORY/${FOLDER}/build/.libs/libSDL2main.so $LIB_DIR fi -cp $DIRECTORY/${FOLDER}/build/.libs/libSDL2.a $LIB_DIR +if [ -f "$DIRECTORY/$FOLDER/build/.libs/libSDL2.so" ]; then cp $DIRECTORY/${FOLDER}/build/.libs/libSDL2.so $LIB_DIR -cp $DIRECTORY/${FOLDER}/sdl2-config $LIB_DIR/sdl2-config +fi + chmod +x $LIB_DIR/sdl2-config cp -r $DIRECTORY/include $INCLUDE_DIR/SDL2 diff --git a/scripts/setup_wren.sh b/scripts/setup_wren.sh index 2fdd35bc..2e6fdddd 100755 --- a/scripts/setup_wren.sh +++ b/scripts/setup_wren.sh @@ -24,12 +24,12 @@ fi # Undo external makefile flags just in case unset config MAKEFLAGS="--no-print-directory" -echo $config # build the debug version of wren make clean -make ${@:2} verbose=1 config=debug_$1 wren && cp $WREN_DIR/lib/libwren_d.a $LIB_DIR/libwrend.a +CFLAGS=-fvisibility=hidden +make ${@:2} CFLAGS=${CFLAGS} verbose=1 config=debug_$1 wren && cp $WREN_DIR/lib/libwren_d.a $LIB_DIR/libwrend.a # build the release version of wren make clean -make ${@:2} verbose=1 config=release_$1 wren && cp $WREN_DIR/lib/libwren.a $LIB_DIR/libwren.a +make ${@:2} CFLAGS=${CFLAGS} verbose=1 config=release_$1 wren && cp $WREN_DIR/lib/libwren.a $LIB_DIR/libwren.a # Copy the wren.h to our includes cp $WREN_DIR/src/include/wren.h $INCLUDE_DIR/wren.h diff --git a/src/audio/api.c b/src/audio/api.c new file mode 100644 index 00000000..28316485 --- /dev/null +++ b/src/audio/api.c @@ -0,0 +1,49 @@ +internal CHANNEL_REF +AUDIO_API_channelCreate( + DOME_Context ctx, + CHANNEL_mix mix, + CHANNEL_callback update, + CHANNEL_callback finish, + void* userdata) { + + AUDIO_ENGINE* engine = ((ENGINE*)ctx)->audioEngine; + return AUDIO_ENGINE_channelInit( + engine, + mix, + update, + finish, + userdata + ); + } + +internal void +AUDIO_API_stop(CHANNEL_REF ref) { + AUDIO_ENGINE* engine = ref.engine; + AUDIO_ENGINE_stop(engine, &ref); +} + + +internal void +AUDIO_API_setState(CHANNEL_REF ref, CHANNEL_STATE state) { + AUDIO_ENGINE* engine = ref.engine; + AUDIO_ENGINE_setState(engine, &ref, state); +} +internal CHANNEL_STATE +AUDIO_API_getState(CHANNEL_REF ref) { + AUDIO_ENGINE* engine = ref.engine; + return AUDIO_ENGINE_getState(engine, &ref); +} + +internal void* +AUDIO_API_getData(CHANNEL_REF ref) { + AUDIO_ENGINE* engine = ref.engine; + return AUDIO_ENGINE_getData(engine, &ref); +} + + +AUDIO_API_v0 audio_v0 = { + .channelCreate = AUDIO_API_channelCreate, + .setState = AUDIO_API_setState, + .stop = AUDIO_API_stop, + .getData = AUDIO_API_getData +}; diff --git a/src/audio/channel.c b/src/audio/channel.c new file mode 100644 index 00000000..83bb20fb --- /dev/null +++ b/src/audio/channel.c @@ -0,0 +1,325 @@ +internal inline void* +CHANNEL_getData(CHANNEL* channel) { + assert(channel != NULL); + return channel->userdata; +} + +internal inline void +CHANNEL_setState(CHANNEL* channel, CHANNEL_STATE state) { + assert(channel != NULL); + channel->state = state; +} + +internal inline CHANNEL_STATE +CHANNEL_getState(CHANNEL* channel) { + assert(channel != NULL); + return channel->state; +} + +internal inline void +CHANNEL_requestStop(CHANNEL* channel) { + assert(channel != NULL); + channel->stopRequested = true; +} + +internal inline bool +CHANNEL_isPlaying(CHANNEL* channel) { + assert(channel != NULL); + return channel->state == CHANNEL_PLAYING + || channel->state == CHANNEL_STOPPING + || channel->state == CHANNEL_VIRTUALIZING; +} + +internal inline bool +CHANNEL_hasStopRequested(CHANNEL* channel) { + assert(channel != NULL); + return channel->stopRequested; +} + +internal void +AUDIO_ENGINE_setPosition(AUDIO_ENGINE* engine, CHANNEL_REF* ref, size_t position) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(engine, ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); + channel->new.position = mid(0, position, channel->audio->length); + channel->new.resetPosition = true; +} + +internal void +AUDIO_ENGINE_setState(AUDIO_ENGINE* engine, CHANNEL_REF* ref, CHANNEL_STATE state) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(engine, ref, &base)) { + return; + } + CHANNEL_setState(base, state); +} + +internal CHANNEL_STATE +AUDIO_ENGINE_getState(AUDIO_ENGINE* engine, CHANNEL_REF* ref) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(engine, ref, &base)) { + return CHANNEL_STOPPED; + } + return CHANNEL_getState(base); +} + + +#define AUDIO_CHANNEL_GETTER(fieldName, method, fieldType, defaultValue) \ + internal fieldType \ + AUDIO_ENGINE_get##method(AUDIO_ENGINE* engine, CHANNEL_REF* ref) { \ + CHANNEL* base; \ + if (!AUDIO_ENGINE_get(engine, ref, &base)) { \ + return defaultValue; \ + } \ + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); \ + return channel->fieldName; \ + } + +#define AUDIO_CHANNEL_SETTER(fieldName, method, fieldType) \ + internal void \ + AUDIO_ENGINE_set##method(AUDIO_ENGINE* engine, CHANNEL_REF* ref, fieldType value) { \ + CHANNEL* base; \ + if (!AUDIO_ENGINE_get(engine, ref, &base)) { \ + return; \ + } \ + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); \ + channel->fieldName = value; \ + } +#define AUDIO_CHANNEL_SETTER_WITH_RANGE(fieldName, method, fieldType, min, max) \ + internal void \ + AUDIO_ENGINE_set##method(AUDIO_ENGINE* engine, CHANNEL_REF* ref, fieldType value) { \ + CHANNEL* base; \ + if (!AUDIO_ENGINE_get(engine, ref, &base)) { \ + return; \ + } \ + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); \ + channel->fieldName = fmid(min, value, max); \ + } + +// volume +AUDIO_CHANNEL_SETTER_WITH_RANGE(new.volume, Volume, float, 0.0f, 1.0f) +AUDIO_CHANNEL_GETTER(new.volume, Volume, float, 0.0f) +// pan +AUDIO_CHANNEL_SETTER_WITH_RANGE(new.pan, Pan, float, -1.0f, 1.0f) +AUDIO_CHANNEL_GETTER(new.pan, Pan, float, 0.0f) + +// loop +AUDIO_CHANNEL_SETTER(new.loop, Loop, bool) +AUDIO_CHANNEL_GETTER(new.loop, Loop, bool, false) +// position +AUDIO_CHANNEL_GETTER(new.position, Position, size_t, 0) + + +internal void +AUDIO_CHANNEL_finish(CHANNEL_REF ref, WrenVM* vm) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(ref.engine, &ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); + assert(channel != NULL); + if (channel->audioHandle != NULL) { + wrenReleaseHandle(vm, channel->audioHandle); + channel->audioHandle = NULL; + } + free(channel->soundId); + free(channel); +} + +internal void +AUDIO_CHANNEL_commit(AUDIO_CHANNEL* channel) { + size_t position = channel->current.position; + if (channel->new.resetPosition) { + position = channel->new.position; + channel->new.resetPosition = false; + } + channel->current = channel->new; + channel->current.position = position; + channel->new = channel->current; +} + +internal void +AUDIO_CHANNEL_update(CHANNEL_REF ref, WrenVM* vm) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(ref.engine, &ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); + switch (CHANNEL_getState(base)) { + case CHANNEL_INITIALIZE: + CHANNEL_setState(base, CHANNEL_TO_PLAY); + // Fallthrough + case CHANNEL_DEVIRTUALIZE: + case CHANNEL_TO_PLAY: + if (CHANNEL_getState(base) == CHANNEL_DEVIRTUALIZE) { + // We might do special things to de-virtualize a channel + } + if (channel->audio == NULL) { + CHANNEL_setState(base, CHANNEL_LOADING); + break; + } + // We assume data is loaded by now. + CHANNEL_setState(base, CHANNEL_PLAYING); + AUDIO_CHANNEL_commit(channel); + break; + case CHANNEL_LOADING: + if (channel->audio != NULL) { + CHANNEL_setState(base, CHANNEL_TO_PLAY); + } + break; + case CHANNEL_PLAYING: + AUDIO_CHANNEL_commit(channel); + if (CHANNEL_isPlaying(base) == false || CHANNEL_hasStopRequested(base)) { + CHANNEL_setState(base, CHANNEL_STOPPING); + } + break; + case CHANNEL_STOPPING: + if (channel->fade) { + channel->new.volume -= 0.1; + } else { + channel->new.volume = 0; + } + if (channel->new.volume <= 0) { + channel->new.volume = 0; + CHANNEL_setState(base, CHANNEL_STOPPED); + } + AUDIO_CHANNEL_commit(channel); + break; + case CHANNEL_STOPPED: + AUDIO_CHANNEL_commit(channel); + break; + default: break; + } +} + +internal void +AUDIO_CHANNEL_mix(CHANNEL_REF ref, float* stream, size_t totalSamples) { + CHANNEL* base; + if (!AUDIO_ENGINE_get(ref.engine, &ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)CHANNEL_getData(base); + if (channel->audio == NULL) { + return; + } + AUDIO_DATA* audio = channel->audio; + float* startReadCursor = (float*)(audio->buffer); + float* readCursor = startReadCursor + channel->current.position * channels; + float* writeCursor = stream; + size_t length = audio->length; + + size_t samplesToWrite = channel->current.loop ? totalSamples : min(totalSamples, length - channel->current.position); + float volume = channel->current.volume; + float targetPan = channel->current.pan; + float actualVolume = channel->actualVolume; + float actualPan = channel->actualPan; + + bool blendVolume = actualVolume != volume; + bool blendPan = actualPan != targetPan; + + + for (size_t i = 0; i < samplesToWrite; i++) { + // We have to lerp the volume and pan change across the whole sample buffer + // or we get a clicking sound. + float f = i / (float)samplesToWrite; + float currentVolume = actualVolume; + if (blendVolume) { + currentVolume = lerp(actualVolume, volume, f); + } + float currentPan = actualPan; + if (blendPan) { + currentPan = lerp(actualPan, targetPan, f); + } + float pan = (currentPan + 1.0f) * M_PI / 4.0f; // Channel pan is [-1,1] real pan needs to be [0,1] + + // We have to advance the cursor after each read and write + // Read/Write left + *(writeCursor++) = *(readCursor++) * cos(pan) * currentVolume; + // Read/Write right + *(writeCursor++) = *(readCursor++) * sin(pan) * currentVolume; + + channel->current.position++; + if (channel->current.position >= length) { + if (channel->current.loop) { + channel->current.position = 0; + readCursor = startReadCursor; + } else { + break; + } + } + } + channel->actualVolume = channel->current.volume; + channel->actualPan = channel->current.pan; + if (!channel->current.loop && channel->current.position >= length) { + CHANNEL_setState(base, CHANNEL_STOPPED); + } +} +internal float* +resample(float* data, size_t srcLength, uint64_t srcFrequency, uint64_t targetFrequency, size_t* destLength) { + // Compute GCD of both frequencies + uint64_t divisor = gcd(srcFrequency, targetFrequency); + + uint64_t L = targetFrequency / divisor; + uint64_t M = srcFrequency / divisor; + + size_t sampleCount = srcLength; + + size_t tempSampleCount = sampleCount * L; + float* tempData = calloc(tempSampleCount, bytesPerSample); + if (tempData == NULL) { + return NULL; + } + + size_t destSampleCount = ceil(tempSampleCount / M) + 1; + *destLength = destSampleCount; + float* destData = malloc(destSampleCount * bytesPerSample); + if (destData == NULL) { + return NULL; + } + // Space out samples in temp data + float* sampleCursor = data; + float* writeCursor = tempData; + for (size_t i = 0; i < sampleCount * L; i++) { + size_t index = channels * (i / L); + *(writeCursor++) = sampleCursor[index]; + *(writeCursor++) = sampleCursor[index + 1]; + } + + // TODO: Low-pass filter over the data (optional - but recommended) + + // decimate by M + sampleCursor = tempData; + writeCursor = destData; + + for(size_t i = 0; i < tempSampleCount; i += M) { + *(writeCursor++) = sampleCursor[i*2]; + *(writeCursor++) = sampleCursor[i*2+1]; + } + + free(tempData); + return destData; +} + +internal CHANNEL_REF +AUDIO_CHANNEL_new(AUDIO_ENGINE* engine, const char* soundId) { + + AUDIO_CHANNEL* data = malloc(sizeof(AUDIO_CHANNEL)); + data->soundId = strdup(soundId); + struct AUDIO_CHANNEL_PROPS props = {0, 0, 0, 0, 0}; + data->current = data->new = props; + data->actualVolume = 0.0f; + data->actualPan = 0.0f; + data->audio = NULL; + + CHANNEL_REF ref = AUDIO_ENGINE_channelInit( + engine, + AUDIO_CHANNEL_mix, + AUDIO_CHANNEL_update, + AUDIO_CHANNEL_finish, + data + ); + return ref; +} + diff --git a/src/audio/engine.c b/src/audio/engine.c new file mode 100644 index 00000000..8ebd08be --- /dev/null +++ b/src/audio/engine.c @@ -0,0 +1,231 @@ +#define AUDIO_CHANNEL_START 0 +#define SAMPLE_RATE 44100 + +global_variable const uint16_t channels = 2; +global_variable const uint16_t bytesPerSample = 4 * 2; // 4-byte float * 2 channels; + +typedef struct AUDIO_ENGINE_t { + SDL_AudioDeviceID deviceId; + SDL_AudioSpec spec; + float* scratchBuffer; + size_t scratchBufferSize; + + TABLE pending; + TABLE playing; + CHANNEL_ID nextId; +} AUDIO_ENGINE; + +// audio callback function +// Allows SDL to "pull" data into the output buffer +// on a seperate thread. We need to be pretty efficient +// here as it holds a lock. +void AUDIO_ENGINE_mix(void* userdata, + Uint8* stream, + int outputBufferSize) { + AUDIO_ENGINE* audioEngine = userdata; + + size_t totalSamples = outputBufferSize / bytesPerSample; + + SDL_memset(stream, 0, outputBufferSize); + + float* scratchBuffer = audioEngine->scratchBuffer; + size_t bufferSampleSize = audioEngine->scratchBufferSize; + TABLE_ITERATOR iter; + TABLE_iterInit(&iter); + CHANNEL* channel; + while (TABLE_iterate(&(audioEngine->playing), &iter)) { + channel = iter.value; + if (!CHANNEL_isPlaying(channel)) { + continue; + } + + assert(channel != NULL); + size_t requestServed = 0; + float* writeCursor = (float*)(stream); + CHANNEL_mix mixFn = channel->mix; + + while (CHANNEL_isPlaying(channel) && requestServed < totalSamples) { + SDL_memset(scratchBuffer, 0, bufferSampleSize * bytesPerSample); + size_t requestSize = min(bufferSampleSize, totalSamples - requestServed); + mixFn(channel->ref, scratchBuffer, requestSize); + requestServed += requestSize; + float* copyCursor = scratchBuffer; + float* endPoint = copyCursor + bufferSampleSize * channels; + for (; copyCursor < endPoint; copyCursor++) { + *(writeCursor++) += *copyCursor; + } + } + } +} + +internal AUDIO_ENGINE* +AUDIO_ENGINE_init(void) { + SDL_InitSubSystem(SDL_INIT_AUDIO); + AUDIO_ENGINE* engine = malloc(sizeof(AUDIO_ENGINE)); + + // zero is reserved for uninitialized. + engine->nextId = 1; + + // SETUP player + // set the callback function + (engine->spec).freq = SAMPLE_RATE; + (engine->spec).format = AUDIO_F32LSB; + (engine->spec).channels = channels; // TODO: consider mono/stereo + (engine->spec).samples = AUDIO_BUFFER_SIZE; + (engine->spec).callback = AUDIO_ENGINE_mix; + (engine->spec).userdata = engine; + + // open audio device + engine->deviceId = SDL_OpenAudioDevice(NULL, 0, &(engine->spec), NULL, 0); + // TODO: Handle if we can't get a device! + + engine->scratchBuffer = calloc(AUDIO_BUFFER_SIZE, bytesPerSample); + if (engine->scratchBuffer != NULL) { + engine->scratchBufferSize = AUDIO_BUFFER_SIZE; + } + + // Unpause audio so we can begin taking over the buffer + SDL_PauseAudioDevice(engine->deviceId, 0); + + TABLE_init(&(engine->pending)); + TABLE_init(&(engine->playing)); + + return engine; +} + +internal bool +AUDIO_ENGINE_get(AUDIO_ENGINE* engine, CHANNEL_REF* ref, CHANNEL** channel) { + CHANNEL_ID id = ref->id; + if (id == 0) { + return false; + } + bool result = TABLE_get(&engine->playing, id, channel); + if (!result) { + result = TABLE_get(&engine->pending, id, channel); + } + return result; +} + +internal void +AUDIO_ENGINE_lock(AUDIO_ENGINE* engine) { + SDL_LockAudioDevice(engine->deviceId); +} + +internal void +AUDIO_ENGINE_unlock(AUDIO_ENGINE* engine) { + SDL_UnlockAudioDevice(engine->deviceId); +} + +internal CHANNEL_REF +AUDIO_ENGINE_channelInit( + AUDIO_ENGINE* engine, + CHANNEL_mix mix, + CHANNEL_callback update, + CHANNEL_callback finish, + void* userdata) { + + CHANNEL_ID id = engine->nextId++; + CHANNEL_REF ref = { + .id = id, + .engine = engine + }; + CHANNEL channel = { + .state = CHANNEL_INITIALIZE, + .mix = mix, + .update = update, + .finish = finish, + .userdata = userdata, + .ref = ref + }; + TABLE_set(&engine->pending, id, channel); + + return ref; +} + +internal void +AUDIO_ENGINE_update(AUDIO_ENGINE* engine, WrenVM* vm) { + TABLE_ITERATOR iter; + TABLE_iterInit(&iter); + CHANNEL* channel; + AUDIO_ENGINE_lock(engine); + TABLE_addAll(&engine->playing, &engine->pending); + while (TABLE_iterate(&(engine->playing), &iter)) { + channel = iter.value; + if (channel->update != NULL) { + channel->update(channel->ref, vm); + } + if (channel->state == CHANNEL_STOPPED) { + if (channel->finish != NULL) { + channel->finish(channel->ref, vm); + } + TABLE_delete(&engine->playing, channel->ref.id); + } + } + AUDIO_ENGINE_unlock(engine); + TABLE_free(&engine->pending); + // DEBUG_LOG("Capacity: %u / %u", engine->playing.items, (engine->playing).capacity); +} + +internal void +AUDIO_ENGINE_stop(AUDIO_ENGINE* engine, CHANNEL_REF* ref) { + CHANNEL* channel; + AUDIO_ENGINE_get(engine, ref, &channel); + if (channel != NULL) { + CHANNEL_requestStop(channel); + } +} + +internal void* +AUDIO_ENGINE_getData(AUDIO_ENGINE* engine, CHANNEL_REF* ref) { + CHANNEL* channel; + AUDIO_ENGINE_get(engine, ref, &channel); + if (channel != NULL) { + return CHANNEL_getData(channel); + } + return NULL; +} + +internal void +AUDIO_ENGINE_stopAll(AUDIO_ENGINE* engine) { + TABLE_ITERATOR iter; + TABLE_iterInit(&iter); + CHANNEL* channel; + while (TABLE_iterate(&(engine->playing), &iter)) { + channel = iter.value; + CHANNEL_requestStop(channel); + } + TABLE_iterInit(&iter); + while (TABLE_iterate(&(engine->pending), &iter)) { + channel = iter.value; + CHANNEL_requestStop(channel); + } +} + + +internal void +AUDIO_ENGINE_pause(AUDIO_ENGINE* engine) { + SDL_PauseAudioDevice(engine->deviceId, 1); +} + +internal void +AUDIO_ENGINE_resume(AUDIO_ENGINE* engine) { + SDL_PauseAudioDevice(engine->deviceId, 0); +} + +internal void +AUDIO_ENGINE_halt(AUDIO_ENGINE* engine) { + if (engine != NULL) { + SDL_PauseAudioDevice(engine->deviceId, 1); + SDL_CloseAudioDevice(engine->deviceId); + } +} + +internal void +AUDIO_ENGINE_free(AUDIO_ENGINE* engine) { + // We might need to free contained audio here + AUDIO_ENGINE_halt(engine); + free(engine->scratchBuffer); + TABLE_free(&engine->playing); + TABLE_free(&engine->pending); +} + diff --git a/src/audio/engine.h b/src/audio/engine.h new file mode 100644 index 00000000..a49825cc --- /dev/null +++ b/src/audio/engine.h @@ -0,0 +1,55 @@ +typedef enum { + AUDIO_TYPE_UNKNOWN, + AUDIO_TYPE_WAV, + AUDIO_TYPE_OGG +} AUDIO_TYPE; + +struct CHANNEL_t { + volatile CHANNEL_STATE state; + CHANNEL_REF ref; + bool stopRequested; + CHANNEL_mix mix; + CHANNEL_callback update; + CHANNEL_callback finish; + + void* userdata; +}; + +typedef struct { + SDL_AudioSpec spec; + AUDIO_TYPE audioType; + // Length is the number of LR samples + uint32_t length; + // Audio is stored as a stream of interleaved normalised values from [-1, 1) + float* buffer; +} AUDIO_DATA; + +struct AUDIO_CHANNEL_PROPS { + // Control variables + bool loop; + // Playback variables + float volume; + float pan; + // Position is the sample value to play next + volatile size_t position; + bool resetPosition; +}; + +typedef struct { + struct AUDIO_CHANNEL_PROPS current; + struct AUDIO_CHANNEL_PROPS new; + char* soundId; + float actualVolume; + float actualPan; + bool fade; + + AUDIO_DATA* audio; + WrenHandle* audioHandle; +} AUDIO_CHANNEL; + +internal void AUDIO_CHANNEL_mix(CHANNEL_REF ref, float* stream, size_t totalSamples); +internal void AUDIO_CHANNEL_update(CHANNEL_REF ref, WrenVM* vm); +internal void AUDIO_CHANNEL_finish(CHANNEL_REF ref, WrenVM* vm); +internal void CHANNEL_requestStop(CHANNEL* channel); +internal void* CHANNEL_getData(CHANNEL* channel); +internal inline bool CHANNEL_isPlaying(CHANNEL* channel); diff --git a/src/audio/hashmap.c b/src/audio/hashmap.c new file mode 100644 index 00000000..f7d6c289 --- /dev/null +++ b/src/audio/hashmap.c @@ -0,0 +1,222 @@ +// Hash table based on the implementation in "Crafting Interpreters" +// as well as the Wren VM. + +#define TABLE_MAX_LOAD 0.75 +#define NIL_KEY 0 + +#define IS_TOMBSTONE(entry) ((entry)->value.state == CHANNEL_LAST) +#define IS_EMPTY(entry) ((entry)->value.state == CHANNEL_INVALID) + +// Thomas Wang, Integer Hash Functions. +// http://www.concentric.net/~Ttwang/tech/inthash.htm +internal inline uint32_t +hashBits(uint64_t hash) +{ + // Wren VM cites v8's computeLongHash() function which in turn cites: + // Thomas Wang, Integer Hash Functions. + // http://www.concentric.net/~Ttwang/tech/inthash.htm + hash = ~hash + (hash << 18); // hash = (hash << 18) - hash - 1; + hash = hash ^ (hash >> 31); + hash = hash * 21; // hash = (hash + (hash << 2)) + (hash << 4); + hash = hash ^ (hash >> 11); + hash = hash + (hash << 6); + hash = hash ^ (hash >> 22); + return (uint32_t)(hash & 0x3fffffff); +} + +global_variable const CHANNEL TOMBSTONE = { + .state = CHANNEL_LAST, + .ref = { + .id = NIL_KEY + } +}; +global_variable const CHANNEL EMPTY_CHANNEL = { + .state = CHANNEL_INVALID, + .ref = { + .id = NIL_KEY + } +}; + +typedef struct { + uint32_t next; + uint32_t found; + bool done; + CHANNEL* value; +} TABLE_ITERATOR; + +void TABLE_iterInit(TABLE_ITERATOR* iter) { + iter->next = 0; + iter->found = 0; + iter->done = false; + iter->value = NULL; +} + +typedef struct { + CHANNEL_ID key; + CHANNEL value; +} ENTRY; + +typedef struct { + uint32_t items; + uint32_t count; + uint32_t capacity; + ENTRY* entries; +} TABLE; + +void TABLE_init(TABLE* table) { + table->count = 0; + table->items = 0; + table->capacity = 0; + table->entries = NULL; +} + +void TABLE_free(TABLE* table) { + if (table->entries != NULL) { + free(table->entries); + } + TABLE_init(table); +} + +internal bool +TABLE_iterate(TABLE* table, TABLE_ITERATOR* iter) { + CHANNEL* value = NULL; + if (iter->found >= table->items) { + iter->done = true; + } + if (!iter->done) { + for (uint32_t i = iter->next; i < table->capacity; i++) { + ENTRY* entry = &table->entries[i]; + if (entry->key == NIL_KEY) { + continue; + } + iter->next = i + 1; + iter->found++; + value = &entry->value; + break; + } + iter->value = value; + } + return !iter->done; +} + +internal bool +TABLE_findEntry(ENTRY* entries, uint32_t capacity, CHANNEL_ID key, ENTRY** result) { + if (capacity == 0) { + return false; + } + + uint32_t startIndex = hashBits(key) % capacity; + uint32_t index = startIndex; + ENTRY* tombstone = NULL; + do { + ENTRY* entry = &(entries[index]); + if (entry->key == NIL_KEY) { + if (IS_EMPTY(entry)) { + *result = tombstone != NULL ? tombstone : entry; + return false; + } else { + if (tombstone == NULL) { + tombstone = entry; + } + } + } else if (entry->key == key) { + *result = entry; + return true; + } + + index = (index + 1) % capacity; + } while (index != startIndex); + *result = tombstone; + return false; +} + +internal void +TABLE_resize(TABLE* table, uint32_t capacity) { + ENTRY* entries = malloc(sizeof(ENTRY) * capacity); + for (uint32_t i = 0; i < capacity; i++) { + entries[i].key = NIL_KEY; + entries[i].value = EMPTY_CHANNEL; + } + table->count = 0; + for (uint32_t i = 0; i < table->capacity; i++) { + ENTRY* entry = &table->entries[i]; + if (entry->key == NIL_KEY) { + continue; + } + ENTRY* dest = NULL; + bool found = TABLE_findEntry(entries, capacity, entry->key, &dest); + if (!found) { + assert(dest != NULL); + dest->key = entry->key; + dest->value = entry->value; + table->count++; + } + } + free(table->entries); + + table->capacity = capacity; + table->entries = entries; +} + +internal CHANNEL* +TABLE_set(TABLE* table, CHANNEL_ID key, CHANNEL channel) { + if ((table->items + 1) > table->capacity * TABLE_MAX_LOAD) { + uint32_t capacity = table->capacity < 4 ? 4 : table->capacity * 2; + TABLE_resize(table, capacity); + } + ENTRY* entry = NULL; + bool found = TABLE_findEntry(table->entries, table->capacity, key, &entry); + if (found) { + entry->value = channel; + } else { + if (!IS_TOMBSTONE(entry)) { + table->count++; + } + table->items++; + entry->key = key; + entry->value = channel; + } + + return &(entry->value); +} + +internal bool +TABLE_get(TABLE* table, CHANNEL_ID key, CHANNEL** channel) { + if (table->count == 0) { + *channel = NULL; + return false; + } + ENTRY* entry; + bool found = TABLE_findEntry(table->entries, table->capacity, key, &entry); + if (found) { + *channel = &entry->value; + return true; + } + return false; +} + +internal bool +TABLE_delete(TABLE* table, CHANNEL_ID key) { + if (table->count == 0) { + return false; + } + ENTRY* entry; + bool found = TABLE_findEntry(table->entries, table->capacity, key, &entry); + if (found) { + table->items--; + entry->key = NIL_KEY; + entry->value = TOMBSTONE; + return true; + } + return false; +} + +internal void +TABLE_addAll(TABLE* dest, TABLE* src) { + for (uint32_t i = 0; i < src->capacity; i++) { + ENTRY* entry = &src->entries[i]; + if (entry->key != NIL_KEY) { + TABLE_set(dest, entry->key, entry->value); + } + } +} diff --git a/src/audio_types.c b/src/audio_types.c deleted file mode 100644 index 891e3e93..00000000 --- a/src/audio_types.c +++ /dev/null @@ -1,6 +0,0 @@ -typedef enum { - AUDIO_TYPE_UNKNOWN, - AUDIO_TYPE_WAV, - AUDIO_TYPE_OGG -} AUDIO_TYPE; - diff --git a/src/debug.c b/src/debug.c index bc0c740e..61a0b8a6 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1,3 +1,4 @@ +#define DEBUG_LOG(string, ...) printf(string"\n", __VA_ARGS__) internal char* DEBUG_printWrenType(WrenType type) { diff --git a/src/engine.c b/src/engine.c index 71f31dbb..fbb1aefb 100644 --- a/src/engine.c +++ b/src/engine.c @@ -1,3 +1,10 @@ +internal void +getColorComponents(uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) { + *r = color & 0xFF; + *g = (color & (0xFF << 8)) >> 8; + *b = (color & (0xFF << 16)) >> 16; +} + internal int ENGINE_record(void* ptr) { // Thread: Seperate gif record @@ -59,17 +66,15 @@ ENGINE_openLogFile(ENGINE* engine) { } internal void -ENGINE_printLog(ENGINE* engine, char* line, ...) { - // Args is mutated by each vsnprintf call, - // so it needs to be reinitialised. +ENGINE_printLogVariadic(ENGINE* engine, const char* line, va_list argList) { va_list args; - va_start(args, line); + va_copy(args, argList); size_t bufSize = vsnprintf(NULL, 0, line, args) + 1; va_end(args); char buffer[bufSize]; buffer[0] = '\0'; - va_start(args, line); + va_copy(args, argList); vsnprintf(buffer, bufSize, line, args); va_end(args); @@ -86,6 +91,16 @@ ENGINE_printLog(ENGINE* engine, char* line, ...) { } } +internal void +ENGINE_printLog(ENGINE* engine, char* line, ...) { + // Args is mutated by each vsnprintf call, + // so it needs to be reinitialised. + va_list args; + va_start(args, line); + ENGINE_printLogVariadic(engine, line, args); + va_end(args); +} + internal ENGINE_WRITE_RESULT ENGINE_writeFile(ENGINE* engine, const char* path, const char* buffer, size_t length) { const char* fullPath; @@ -224,6 +239,9 @@ ENGINE_init(ENGINE* engine) { engine->argv = NULL; engine->argc = 0; + // TODO: handle if we can't allocate memory. + PLUGIN_COLLECTION_init(engine); + return engine; } @@ -304,6 +322,8 @@ ENGINE_free(ENGINE* engine) { ENGINE_finishAsync(engine); + PLUGIN_COLLECTION_free(engine); + if (engine->audioEngine) { AUDIO_ENGINE_free(engine->audioEngine); free(engine->audioEngine); @@ -362,6 +382,7 @@ ENGINE_pget(ENGINE* engine, int64_t x, int64_t y) { } return 0xFF000000; } + inline internal void ENGINE_pset(ENGINE* engine, int64_t x, int64_t y, uint32_t c) { @@ -373,25 +394,24 @@ ENGINE_pset(ENGINE* engine, int64_t x, int64_t y, uint32_t c) { int32_t width = engine->canvas.width; DOME_RECT zone = engine->canvas.clip; - if ((c & (0xFF << 24)) == 0) { + uint8_t newA = ((0xFF000000 & c) >> 24); + + if (newA == 0) { return; } else if (zone.x <= x && x < zone.x + zone.w && zone.y <= y && y < zone.y + zone.h) { - if (((c & (0xFF << 24)) >> 24) < 0xFF) { + if (newA < 0xFF) { uint32_t current = ((uint32_t*)(engine->canvas.pixels))[width * y + x]; + double normA = newA / (double)UINT8_MAX; + double diffA = 1 - normA; - uint16_t newA = (0xFF000000 & c) >> 24; - - uint16_t oldR = (255-newA) * ((0x000000FF & current)); - uint16_t oldG = (255-newA) * ((0x0000FF00 & current) >> 8); - uint16_t oldB = (255-newA) * ((0x00FF0000 & current) >> 16); - uint16_t newR = newA * ((0x000000FF & c)); - uint16_t newG = newA * ((0x0000FF00 & c) >> 8); - uint16_t newB = newA * ((0x00FF0000 & c) >> 16); + uint8_t oldR, oldG, oldB, newR, newG, newB; + getColorComponents(current, &oldR, &oldG, &oldB); + getColorComponents(c, &newR, &newG, &newB); uint8_t a = 0xFF; - uint8_t r = (oldR + newR) / 255; - uint8_t g = (oldG + newG) / 255; - uint8_t b = (oldB + newB) / 255; + uint8_t r = (diffA * oldR + normA * newR); + uint8_t g = (diffA * oldG + normA * newG); + uint8_t b = (diffA * oldB + normA * newB); c = (a << 24) | (b << 16) | (g << 8) | r; } @@ -432,9 +452,9 @@ ENGINE_resizeBlitBuffer(ENGINE* engine, size_t width, size_t height) { return buffer->pixels; } -inline internal unsigned char* +internal unsigned const char* defaultFontLookup(utf8_int32_t codepoint) { - local_persist unsigned char empty[8] = { 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }; + const local_persist unsigned char empty[8] = { 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }; if (codepoint >= 0 && codepoint < 0x7F) { return font8x8_basic[codepoint]; } else if (codepoint >= 0x80 && codepoint <= 0x9F) { diff --git a/src/engine.h b/src/engine.h index 0cc6b4e3..8ed85a3a 100644 --- a/src/engine.h +++ b/src/engine.h @@ -51,7 +51,7 @@ typedef struct { DOME_RECT clip; } CANVAS; -typedef struct { +typedef struct ENGINE_t { ENGINE_RECORDER record; SDL_Window* window; SDL_Renderer *renderer; @@ -61,6 +61,8 @@ typedef struct { PIXEL_BUFFER blitBuffer; ABC_FIFO fifo; MAP moduleMap; + // PLUGIN_MAP pluginMap; + PLUGIN_COLLECTION plugins; mtar_t* tar; bool running; char** argv; diff --git a/src/main.c b/src/main.c index b13795d7..3694506f 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ #define _DEFAULT_SOURCE #define NOMINMAX + #ifndef DOME_VERSION #define DOME_VERSION "0.0.0 - CUSTOM" #endif @@ -20,6 +21,7 @@ #include #include #include +#include #include #ifndef M_PI #define M_PI 3.14159265358979323846 @@ -29,10 +31,16 @@ #include #include -#define internal static +// Import plugin-specific definitions +#define WIN_EXPORT +#include "dome.h" +// project-specific definitions +#define external DOME_EXPORT +#define internal DOME_INTERNAL static #define global_variable static #define local_persist static + #define INIT_TO_ZERO(Type, name)\ Type name;\ memset(&name, 0, sizeof(Type)); @@ -61,7 +69,6 @@ // Used in the io variable, but we need to catch it here global_variable WrenHandle* bufferClass = NULL; -global_variable WrenHandle* audioEngineClass = NULL; global_variable WrenHandle* keyboardClass = NULL; global_variable WrenHandle* mouseClass = NULL; global_variable WrenHandle* gamepadClass = NULL; @@ -76,16 +83,28 @@ global_variable bool DEBUG_MODE = false; global_variable size_t AUDIO_BUFFER_SIZE = 2048; global_variable size_t GIF_SCALE = 1; + + // Game code #include "math.c" #include "strings.c" -#include "audio_types.c" + #include "modules/map.c" + +#include "plugin.h" #include "engine.h" -#include "debug.c" #include "util/font8x8.h" #include "io.c" + +#include "audio/engine.h" +#include "audio/hashmap.c" +#include "audio/engine.c" +#include "audio/channel.c" +#include "audio/api.c" +#include "debug.c" + #include "engine.c" +#include "plugin.c" #include "modules/dome.c" #include "modules/font.c" @@ -95,6 +114,10 @@ global_variable size_t GIF_SCALE = 1; #include "modules/image.c" #include "modules/input.c" #include "modules/json.c" +#include "modules/platform.c" +#include "modules/random.c" +#include "modules/plugin.c" + // Comes last to register modules #include "vm.c" @@ -237,6 +260,9 @@ LOOP_processInput(LOOP_STATE* state) { internal int LOOP_render(LOOP_STATE* state) { + if (PLUGIN_COLLECTION_runHook(state->engine, DOME_PLUGIN_HOOK_PRE_DRAW) != DOME_RESULT_SUCCESS) { + return EXIT_FAILURE; + }; WrenInterpretResult interpreterResult; wrenEnsureSlots(state->vm, 8); wrenSetSlotHandle(state->vm, 0, state->gameClass); @@ -245,6 +271,9 @@ LOOP_render(LOOP_STATE* state) { if (interpreterResult != WREN_RESULT_SUCCESS) { return EXIT_FAILURE; } + if (PLUGIN_COLLECTION_runHook(state->engine, DOME_PLUGIN_HOOK_POST_DRAW) != DOME_RESULT_SUCCESS) { + return EXIT_FAILURE; + }; return EXIT_SUCCESS; @@ -274,23 +303,21 @@ internal int LOOP_update(LOOP_STATE* state) { WrenInterpretResult interpreterResult; + if (PLUGIN_COLLECTION_runHook(state->engine, DOME_PLUGIN_HOOK_PRE_UPDATE) != DOME_RESULT_SUCCESS) { + return EXIT_FAILURE; + }; + wrenEnsureSlots(state->vm, 8); wrenSetSlotHandle(state->vm, 0, state->gameClass); interpreterResult = wrenCall(state->vm, state->updateMethod); if (interpreterResult != WREN_RESULT_SUCCESS) { return EXIT_FAILURE; } + if (PLUGIN_COLLECTION_runHook(state->engine, DOME_PLUGIN_HOOK_POST_UPDATE) != DOME_RESULT_SUCCESS) { + return EXIT_FAILURE; + }; // updateAudio() - if (audioEngineClass != NULL) { - wrenEnsureSlots(state->vm, 3); - wrenSetSlotHandle(state->vm, 0, audioEngineClass); - AUDIO_ENGINE_lock(state->engine->audioEngine); - interpreterResult = wrenCall(state->vm, state->updateMethod); - AUDIO_ENGINE_unlock(state->engine->audioEngine); - if (interpreterResult != WREN_RESULT_SUCCESS) { - return EXIT_FAILURE; - } - } + AUDIO_ENGINE_update(state->engine->audioEngine, state->vm); return EXIT_SUCCESS; } @@ -491,6 +518,7 @@ int main(int argc, char* args[]) } } + chdir(base); if (engine.tar != NULL) { // It is a tar file, we need to look for a "main.wren" entry point. strcpy(pathBuf, mainFileName); @@ -529,6 +557,8 @@ int main(int argc, char* args[]) // Load user game file SDL_Thread* recordThread = NULL; + WrenHandle* initMethod = NULL; + interpreterResult = wrenInterpret(vm, "main", gameFile); free(gameFile); if (interpreterResult != WREN_RESULT_SUCCESS) { @@ -539,28 +569,29 @@ int main(int argc, char* args[]) wrenEnsureSlots(vm, 3); - WrenHandle* initMethod = NULL; initMethod = wrenMakeCallHandle(vm, "init()"); wrenGetVariable(vm, "main", "Game", 0); loop.gameClass = wrenGetSlotHandle(vm, 0); loop.updateMethod = wrenMakeCallHandle(vm, "update()"); loop.drawMethod = wrenMakeCallHandle(vm, "draw(_)"); + SDL_SetRenderDrawColor(engine.renderer, 0x00, 0x00, 0x00, 0xFF); + // Initiate game loop wrenSetSlotHandle(vm, 0, loop.gameClass); interpreterResult = wrenCall(vm, initMethod); - wrenReleaseHandle(vm, initMethod); - initMethod = NULL; if (interpreterResult != WREN_RESULT_SUCCESS) { result = EXIT_FAILURE; goto vm_cleanup; } + // Release this handle if it finished successfully + wrenReleaseHandle(vm, initMethod); + initMethod = NULL; engine.initialized = true; SDL_SetWindowPosition(engine.window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); SDL_ShowWindow(engine.window); - SDL_SetRenderDrawColor(engine.renderer, 0x00, 0x00, 0x00, 0xFF); // Resizing from init must happen before we begin recording if (engine.record.makeGif) { @@ -672,6 +703,9 @@ int main(int argc, char* args[]) } } + // Free resources + ENGINE_reportError(&engine); + if (initMethod != NULL) { wrenReleaseHandle(vm, initMethod); } @@ -682,17 +716,13 @@ int main(int argc, char* args[]) wrenReleaseHandle(vm, bufferClass); } - if (audioEngineClass != NULL) { - wrenReleaseHandle(vm, audioEngineClass); - } - INPUT_release(vm); + AUDIO_ENGINE_halt(engine.audioEngine); + AUDIO_ENGINE_releaseHandles(engine.audioEngine, vm); + cleanup: - // Free resources - ENGINE_reportError(&engine); BASEPATH_free(); - AUDIO_ENGINE_halt(engine.audioEngine); VM_free(vm); result = engine.exit_status; ENGINE_free(&engine); @@ -703,4 +733,3 @@ int main(int argc, char* args[]) return result; } - diff --git a/src/math.c b/src/math.c index 87227aab..497e1264 100644 --- a/src/math.c +++ b/src/math.c @@ -11,55 +11,67 @@ typedef struct { double y; } VEC; -double VEC_len(VEC v) { +internal double +VEC_len(VEC v) { return sqrt(pow(v.x, 2) + pow(v.y, 2)); } -VEC VEC_add(VEC v1, VEC v2) { +internal VEC +VEC_add(VEC v1, VEC v2) { VEC result = { v1.x + v2.x, v1.y + v2.y }; return result; } -VEC VEC_sub(VEC v1, VEC v2) { +internal VEC +VEC_sub(VEC v1, VEC v2) { VEC result = { v1.x - v2.x, v1.y - v2.y }; return result; } -VEC VEC_scale(VEC v, double s) { +internal VEC +VEC_scale(VEC v, double s) { VEC result = { v.x * s, v.y * s }; return result; } -VEC VEC_neg(VEC v) { +internal VEC +VEC_neg(VEC v) { return VEC_scale(v, -1); } -double VEC_dot(VEC v1, VEC v2) { +internal double +VEC_dot(VEC v1, VEC v2) { return v1.x * v2.x + v1.y * v2.y; } -VEC VEC_perp(VEC v) { +internal VEC +VEC_perp(VEC v) { VEC result = { -v.y , v.x }; return result; } +inline internal float +lerp(float a, float b, float f) { + return (a * (1.0 - f)) + (b * f); +} - - -int64_t max(int64_t n1, int64_t n2) { +internal int64_t +max(int64_t n1, int64_t n2) { if (n1 > n2) { return n1; } return n2; } -int64_t min(int64_t n1, int64_t n2) { +internal int64_t +min(int64_t n1, int64_t n2) { if (n1 < n2) { return n1; } return n2; } -double fmid(double n1, double n2, double n3) { +internal double +fmid(double n1, double n2, double n3) { double temp; if (n1 > n3) { temp = n1; @@ -78,7 +90,8 @@ double fmid(double n1, double n2, double n3) { } } -int64_t mid(int64_t n1, int64_t n2, int64_t n3) { +internal int64_t +mid(int64_t n1, int64_t n2, int64_t n3) { int64_t temp; if (n1 > n3) { temp = n1; @@ -96,3 +109,14 @@ int64_t mid(int64_t n1, int64_t n2, int64_t n3) { return n3; } } + +internal uint64_t +gcd(uint64_t a, uint64_t b) { + uint64_t t = b; + while (b != 0) { + t = b; + b = a % b; + a = t; + } + return a; +} diff --git a/src/modules/audio.c b/src/modules/audio.c index 5f52a319..5b090dc7 100644 --- a/src/modules/audio.c +++ b/src/modules/audio.c @@ -1,125 +1,7 @@ -#define AUDIO_CHANNEL_START 2 - -typedef enum { - CHANNEL_INVALID, - CHANNEL_INITIALIZE, - CHANNEL_TO_PLAY, - CHANNEL_DEVIRTUALIZE, - CHANNEL_LOADING, - CHANNEL_PLAYING, - CHANNEL_STOPPING, - CHANNEL_STOPPED, - CHANNEL_VIRTUALIZING, - CHANNEL_VIRTUAL, - CHANNEL_LAST -} CHANNEL_STATE; - -typedef struct { - SDL_AudioSpec spec; - AUDIO_TYPE audioType; - // Length is the number of LR samples - uint32_t length; - // Audio is stored as a stream of interleaved normalised values from [-1, 1) - float* buffer; -} AUDIO_DATA; - - -typedef struct { - CHANNEL_STATE state; - char* soundId; - // Control variables - bool enabled; - bool loop; - - // Playback variables - float volume; - float pan; - - // Position is the sample value to play next - size_t position; - size_t newPosition; - bool resetPosition; - AUDIO_DATA* audio; -} AUDIO_CHANNEL; - -typedef struct { - size_t count; - AUDIO_CHANNEL* channels[]; -} AUDIO_CHANNEL_LIST; - -typedef struct AUDIO_ENGINE_t { - SDL_AudioDeviceID deviceId; - SDL_AudioSpec spec; - AUDIO_CHANNEL_LIST* channelList; -} AUDIO_ENGINE; - -const uint16_t channels = 2; -const uint16_t bytesPerSample = 2 * 2 /* channels */; - -// audio callback function -// Allows SDL to "pull" data into the output buffer -// on a seperate thread. We need to be pretty efficient -// here as it holds a lock. -internal void -AUDIO_ENGINE_capture(WrenVM* vm) { - if (audioEngineClass == NULL) { - wrenGetVariable(vm, "audio", "AudioEngine", 0); - audioEngineClass = wrenGetSlotHandle(vm, 0); - } -} - -void AUDIO_ENGINE_mix(void* userdata, - Uint8* stream, - int outputBufferSize) { - AUDIO_ENGINE* audioEngine = userdata; - uint32_t totalSamples = outputBufferSize / bytesPerSample; - - int16_t* writeCursor = (int16_t*)(stream); - SDL_memset(writeCursor, 0, outputBufferSize); - - int32_t samplesToWrite = totalSamples;// - samplesQueued; - int totalChannels = 0; - - // Get channel - for (int i = 0; i < samplesToWrite; i++) { - int totalEnabled = 0; - float left = 0; - float right = 0; - AUDIO_CHANNEL_LIST* channelList = audioEngine->channelList; - for (size_t c = 0; c < channelList->count; c++) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)(channelList->channels[c]); - if (channel != NULL && channel->audio != NULL) { - AUDIO_DATA* audio = channel->audio; - if (channel->enabled) { - totalEnabled++; - float* readCursor = (float*)(audio->buffer); - readCursor += channel->position * channels; - float volume = channel->volume; - float pan = (channel->pan + 1) * M_PI / 4.0; // Channel pan is [-1,1] real pan needs to be [0,1] - - left += readCursor[0] * cos(pan) * volume; - right += readCursor[1] * sin(pan) * volume; - } - channel->position++; - if (channel->loop && channel->position >= audio->length) { - channel->position = 0; - } - channel->enabled = channel->enabled && channel->position < audio->length; - } - } - if (totalEnabled > 1) { - left = (float)tanh(left); ///= totalEnabled; - right = (float)tanh(right); //= totalEnabled; - } - if (totalEnabled > 0) { - writeCursor[i*2] = (int16_t)(left * INT16_MAX); - writeCursor[i*2+1] = (int16_t)(right * INT16_MAX); - } - totalChannels = max(totalEnabled, totalChannels); - } -} +internal float* resample(float* data, size_t srcLength, uint64_t srcFrequency, uint64_t targetFrequency, size_t* destLength); -internal void AUDIO_allocate(WrenVM* vm) { +internal void +AUDIO_allocate(WrenVM* vm) { wrenEnsureSlots(vm, 1); AUDIO_DATA* data = (AUDIO_DATA*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(AUDIO_DATA)); int length; @@ -127,6 +9,7 @@ internal void AUDIO_allocate(WrenVM* vm) { const char* fileBuffer = wrenGetSlotBytes(vm, 1, &length); int16_t* tempBuffer; + if (strncmp(fileBuffer, "RIFF", 4) == 0 && strncmp(&fileBuffer[8], "WAVE", 4) == 0) { data->audioType = AUDIO_TYPE_WAV; @@ -155,16 +38,16 @@ internal void AUDIO_allocate(WrenVM* vm) { data->spec.channels = channelsInFile; data->spec.freq = freq; - data->spec.format = AUDIO_S16LSB; + data->spec.format = AUDIO_F32LSB; } else { VM_ABORT(vm, "Audio file was of an incompatible format"); return; } - data->buffer = calloc(channels * data->length, sizeof(float)); + data->buffer = calloc(data->length, bytesPerSample); assert(data->buffer != NULL); assert(data->length != UINT32_MAX); - // Process incoming values into an intermediate mixable format + // Process incoming values into a mixable format for (uint32_t i = 0; i < data->length; i++) { data->buffer[i * channels] = (float)(tempBuffer[i * data->spec.channels]) / INT16_MAX; if (data->spec.channels == 1) { @@ -173,6 +56,15 @@ internal void AUDIO_allocate(WrenVM* vm) { data->buffer[i * channels + 1] = (float)(tempBuffer[i * data->spec.channels + 1]) / INT16_MAX; } } + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* audioEngine = engine->audioEngine; + if (data->spec.freq != audioEngine->spec.freq) { + size_t newLength = 0; + void* oldPtr = data->buffer; + data->buffer = resample(data->buffer, data->length, data->spec.freq, audioEngine->spec.freq, &newLength); + data->length = newLength; + free(oldPtr); + } // free the intermediate buffers if (data->audioType == AUDIO_TYPE_WAV) { SDL_FreeWAV((uint8_t*)tempBuffer); @@ -180,12 +72,13 @@ internal void AUDIO_allocate(WrenVM* vm) { free(tempBuffer); } if (DEBUG_MODE) { - ENGINE* engine = wrenGetUserData(vm); DEBUG_printAudioSpec(engine, data->spec, data->audioType); } } -internal void AUDIO_finalize(void* data) { + +internal void +AUDIO_finalize(void* data) { AUDIO_DATA* audioData = (AUDIO_DATA*)data; if (audioData->buffer != NULL) { if (audioData->audioType == AUDIO_TYPE_WAV || audioData->audioType == AUDIO_TYPE_OGG) { @@ -194,233 +87,256 @@ internal void AUDIO_finalize(void* data) { audioData->buffer = NULL; } } -internal void AUDIO_unload(WrenVM* vm) { +internal void +AUDIO_unload(WrenVM* vm) { ASSERT_SLOT_TYPE(vm, 1, FOREIGN, "audio data"); AUDIO_DATA* data = (AUDIO_DATA*)wrenGetSlotForeign(vm, 0); AUDIO_finalize(data); } -internal void AUDIO_getLength(WrenVM* vm) { +internal void +AUDIO_getLength(WrenVM* vm) { AUDIO_DATA* data = (AUDIO_DATA*)wrenGetSlotForeign(vm, 0); wrenEnsureSlots(vm, 1); wrenSetSlotDouble(vm, 0, data->length); } -internal AUDIO_ENGINE* -AUDIO_ENGINE_init(void) { - SDL_InitSubSystem(SDL_INIT_AUDIO); - AUDIO_ENGINE* engine = malloc(sizeof(AUDIO_ENGINE)); - engine->channelList = malloc(sizeof(AUDIO_CHANNEL_LIST) + sizeof(AUDIO_CHANNEL*) * AUDIO_CHANNEL_START); - engine->channelList->count = AUDIO_CHANNEL_START; - for (int i = 0; i < AUDIO_CHANNEL_START; i++) { - engine->channelList->channels[i] = NULL; - } - // SETUP player - // set the callback function - (engine->spec).freq = 44100; - (engine->spec).format = AUDIO_S16LSB; - (engine->spec).channels = channels; // TODO: consider mono/stereo - (engine->spec).samples = AUDIO_BUFFER_SIZE; // Consider making this configurable - (engine->spec).callback = AUDIO_ENGINE_mix; - (engine->spec).userdata = engine; - - // open audio device - engine->deviceId = SDL_OpenAudioDevice(NULL, 0, &(engine->spec), NULL, 0); - // TODO: Handle if we can't get a device! - - // Unpause audio so we can begin taking over the buffer - SDL_PauseAudioDevice(engine->deviceId, 0); - return engine; -} - -internal AUDIO_CHANNEL_LIST* -AUDIO_CHANNEL_LIST_resize(AUDIO_CHANNEL_LIST* list, size_t channels) { - if (list->count < channels) { - list = realloc(list, sizeof(AUDIO_CHANNEL_LIST) + sizeof(AUDIO_CHANNEL*) * channels); - list->count = channels; - for (int i = 0; i < AUDIO_CHANNEL_START; i++) { - list->channels[i] = NULL; - } - } - return list; -} - -internal void AUDIO_ENGINE_lock(AUDIO_ENGINE* engine) { - SDL_LockAudioDevice(engine->deviceId); -} - -internal void AUDIO_ENGINE_unlock(AUDIO_ENGINE* engine) { - SDL_UnlockAudioDevice(engine->deviceId); -} +internal void +AUDIO_CHANNEL_stop(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); -internal void AUDIO_ENGINE_update(WrenVM* vm) { - // We need additional slots to parse a list - wrenEnsureSlots(vm, 3); ENGINE* engine = wrenGetUserData(vm); AUDIO_ENGINE* data = engine->audioEngine; - AUDIO_ENGINE_lock(data); - ASSERT_SLOT_TYPE(vm, 1, LIST, "channels"); - uint8_t soundCount = wrenGetListCount(vm, 1); - data->channelList = AUDIO_CHANNEL_LIST_resize(data->channelList, soundCount); - for (size_t i = 0; i < data->channelList->count; i++) { - if (i < soundCount) { - wrenGetListElement(vm, 1, i, 2); - if (wrenGetSlotType(vm, 2) != WREN_TYPE_NULL) { - data->channelList->channels[i] = wrenGetSlotForeign(vm, 2); - AUDIO_CHANNEL* channel = data->channelList->channels[i]; - if (channel->resetPosition) { - channel->position = channel->newPosition; - channel->resetPosition = false; - } - } - } else { - data->channelList->channels[i] = NULL; - } + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; } - AUDIO_ENGINE_unlock(data); + CHANNEL_requestStop(base); } -internal void AUDIO_ENGINE_pause(AUDIO_ENGINE* engine) { - SDL_PauseAudioDevice(engine->deviceId, 1); -} - -internal void AUDIO_ENGINE_resume(AUDIO_ENGINE* engine) { - SDL_PauseAudioDevice(engine->deviceId, 0); +internal void +AUDIO_ENGINE_wrenStopAll(WrenVM* vm) { + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* audioEngine = engine->audioEngine; + AUDIO_ENGINE_stopAll(audioEngine); } -internal void AUDIO_ENGINE_halt(AUDIO_ENGINE* engine) { - if (engine != NULL) { - SDL_PauseAudioDevice(engine->deviceId, 1); - SDL_CloseAudioDevice(engine->deviceId); +internal void +AUDIO_ENGINE_releaseHandles(AUDIO_ENGINE* audioEngine, WrenVM* vm) { + TABLE_ITERATOR iter; + TABLE_iterInit(&iter); + CHANNEL* channel; + while (TABLE_iterate(&(audioEngine->playing), &iter)) { + channel = iter.value; + channel->state = CHANNEL_STOPPED; + if (channel->finish != NULL) { + channel->finish(channel->ref, vm); + } + } + TABLE_iterInit(&iter); + while (TABLE_iterate(&(audioEngine->pending), &iter)) { + channel = iter.value; + channel->state = CHANNEL_STOPPED; + if (channel->finish != NULL) { + channel->finish(channel->ref, vm); + } } } -internal void AUDIO_ENGINE_free(AUDIO_ENGINE* engine) { - // We might need to free contained audio here - AUDIO_ENGINE_halt(engine); - free(engine->channelList); -} -internal void AUDIO_CHANNEL_allocate(WrenVM* vm) { - wrenEnsureSlots(vm, 1); - AUDIO_CHANNEL* data = (AUDIO_CHANNEL*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(AUDIO_CHANNEL)); +internal void +AUDIO_CHANNEL_allocate(WrenVM* vm) { + wrenEnsureSlots(vm, 2); + CHANNEL_REF* ref = (CHANNEL_REF*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(CHANNEL_REF)); ASSERT_SLOT_TYPE(vm, 1, STRING, "sound id"); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + const char* soundId = wrenGetSlotString(vm, 1); - size_t len = strlen(soundId); - data->soundId = malloc((1 + len) * sizeof(char)); - strcpy(data->soundId, soundId); - data->soundId[len] = '\0'; - - data->state = CHANNEL_INITIALIZE; - data->enabled = false; - data->loop = false; - data->audio = NULL; + *ref = AUDIO_CHANNEL_new(data, soundId); } -internal void AUDIO_CHANNEL_setAudio(WrenVM* vm) { - AUDIO_CHANNEL* data = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); - if (data->state == CHANNEL_INITIALIZE) { +internal void +AUDIO_CHANNEL_setAudio(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = CHANNEL_getData(base); + if (CHANNEL_getState(base) < CHANNEL_PLAYING) { ASSERT_SLOT_TYPE(vm, 1, FOREIGN, "audio"); - data->audio = (AUDIO_DATA*)wrenGetSlotForeign(vm, 1); + channel->audio = (AUDIO_DATA*)wrenGetSlotForeign(vm, 1); + channel->audioHandle = wrenGetSlotHandle(vm, 1); } else { VM_ABORT(vm, "Cannot change audio in channel once initialized"); } } -internal void AUDIO_CHANNEL_setState(WrenVM* vm) { - AUDIO_CHANNEL* data = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_setState(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); ASSERT_SLOT_TYPE(vm, 1, NUM, "state"); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; + } int state = wrenGetSlotDouble(vm, 1); if (state <= CHANNEL_INVALID || state >= CHANNEL_LAST) { VM_ABORT(vm, "Setting invalid channel state"); } - data->state = state; + CHANNEL_setState(base, state); } -internal void AUDIO_CHANNEL_getSoundId(WrenVM* vm) { - AUDIO_CHANNEL* data = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getState(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; + } wrenEnsureSlots(vm, 1); - wrenSetSlotString(vm, 0, data->soundId); + wrenSetSlotDouble(vm, 0, CHANNEL_getState(base)); } -internal void AUDIO_CHANNEL_getState(WrenVM* vm) { - AUDIO_CHANNEL* data = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getId(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + wrenEnsureSlots(vm, 1); - wrenSetSlotDouble(vm, 0, data->state); + wrenSetSlotDouble(vm, 0, ref->id); } -internal void AUDIO_CHANNEL_getLength(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getSoundId(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = CHANNEL_getData(base); wrenEnsureSlots(vm, 1); - wrenSetSlotDouble(vm, 0, channel->audio->length); + wrenSetSlotString(vm, 0, channel->soundId); } -internal void AUDIO_CHANNEL_getPosition(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getLength(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + + CHANNEL* base; + if (!AUDIO_ENGINE_get(data, ref, &base)) { + return; + } + AUDIO_CHANNEL* channel = CHANNEL_getData(base); wrenEnsureSlots(vm, 1); - wrenSetSlotDouble(vm, 0, channel->position); + wrenSetSlotDouble(vm, 0, channel->audio->length); } -internal void AUDIO_CHANNEL_setEnabled(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); - ASSERT_SLOT_TYPE(vm, 1, BOOL, "enabled"); - channel->enabled = wrenGetSlotBool(vm, 1); -} +internal void +AUDIO_CHANNEL_getPosition(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); -internal void AUDIO_CHANNEL_getEnabled(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; wrenEnsureSlots(vm, 1); - wrenSetSlotBool(vm, 0, channel->enabled); + wrenSetSlotDouble(vm, 0, AUDIO_ENGINE_getPosition(data, ref)); } -internal void AUDIO_CHANNEL_setLoop(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_setLoop(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + ASSERT_SLOT_TYPE(vm, 1, BOOL, "loop"); - channel->loop = wrenGetSlotBool(vm, 1); + AUDIO_ENGINE_setLoop(data, ref, wrenGetSlotBool(vm, 1)); } -internal void AUDIO_CHANNEL_getLoop(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getLoop(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; wrenEnsureSlots(vm, 1); - wrenSetSlotBool(vm, 0, channel->loop); + wrenSetSlotBool(vm, 0, AUDIO_ENGINE_getLoop(data, ref)); } -internal void AUDIO_CHANNEL_setPosition(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_setPosition(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; ASSERT_SLOT_TYPE(vm, 1, NUM, "position"); - size_t newPosition = round(wrenGetSlotDouble(vm, 1)); - channel->newPosition = mid(0, newPosition, channel->audio->length); - channel->resetPosition = true; + AUDIO_ENGINE_setPosition(data, ref, round(wrenGetSlotDouble(vm, 1))); } -internal void AUDIO_CHANNEL_setVolume(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); + +internal void +AUDIO_CHANNEL_setVolume(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; ASSERT_SLOT_TYPE(vm, 1, NUM, "volume"); - channel->volume = fmax(0, wrenGetSlotDouble(vm, 1)); + AUDIO_ENGINE_setVolume(data, ref, wrenGetSlotDouble(vm, 1)); } -internal void AUDIO_CHANNEL_getVolume(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); - wrenEnsureSlots(vm, 1); - wrenSetSlotDouble(vm, 0, channel->volume); +internal void +AUDIO_CHANNEL_getVolume(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + wrenSetSlotDouble(vm, 0, AUDIO_ENGINE_getVolume(data, ref)); } -internal void AUDIO_CHANNEL_setPan(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); + +internal void +AUDIO_CHANNEL_setPan(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; + ASSERT_SLOT_TYPE(vm, 1, NUM, "pan"); - channel->pan = fmid(-1.0, wrenGetSlotDouble(vm, 1), 1.0f); + AUDIO_ENGINE_setPan(data, ref, wrenGetSlotDouble(vm, 1)); } -internal void AUDIO_CHANNEL_getPan(WrenVM* vm) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)wrenGetSlotForeign(vm, 0); +internal void +AUDIO_CHANNEL_getPan(WrenVM* vm) { + CHANNEL_REF* ref = (CHANNEL_REF*)wrenGetSlotForeign(vm, 0); + + ENGINE* engine = wrenGetUserData(vm); + AUDIO_ENGINE* data = engine->audioEngine; wrenEnsureSlots(vm, 1); - wrenSetSlotDouble(vm, 0, channel->pan); + wrenSetSlotDouble(vm, 0, AUDIO_ENGINE_getPan(data, ref)); } -internal void AUDIO_CHANNEL_finalize(void* data) { - AUDIO_CHANNEL* channel = (AUDIO_CHANNEL*)data; - free(channel->soundId); -} +internal void +AUDIO_CHANNEL_finalize(void* data) {} internal double dbToVolume(double dB) { @@ -431,3 +347,4 @@ internal double volumeToDb(double volume) { return 20.0 * log10(volume); } + diff --git a/src/modules/audio.wren b/src/modules/audio.wren index 58af326c..6540bcce 100644 --- a/src/modules/audio.wren +++ b/src/modules/audio.wren @@ -13,9 +13,6 @@ foreign class AudioData { foreign length } -// Base interface for audio channels -class AudioChannel {} - class AudioState { static INITIALIZE { 1 } static TO_PLAY { 2 } @@ -28,22 +25,28 @@ class AudioState { static VIRTUAL { 9 } } +// Base interface for audio channels +class AudioChannel {} + // Encapsulates the data of the currently playing channel foreign class SystemChannel is AudioChannel { - construct new(soundId) {} + construct new(soundId) { + // Sensible defaults + volume = 1 + pan = 0 + loop = false + } foreign audio=(value) foreign length foreign soundId + foreign id foreign position foreign position=(v) foreign state foreign state=(value) - foreign enabled=(enable) - foreign enabled - foreign loop=(do) foreign loop @@ -52,115 +55,24 @@ foreign class SystemChannel is AudioChannel { foreign volume=(volume) foreign volume -} - -class AudioChannelFacade is AudioChannel { - construct wrap(id, channel) { - _channel = channel - _length = channel.length - _soundId = channel.soundId - _position = null // This is only set by the user - _volume = 1 - _pan = 0 - _loop = false - _stopRequested = false - _id = id - } - - stop() { - _stopRequested = true - } - - release_() { - _channel = null - } - - update_() { - if (state == AudioState.INITIALIZE) { - _channel.state = AudioState.TO_PLAY - // Fallthrough - } - if (state == AudioState.TO_PLAY || state == AudioState.DEVIRTUALIZE) { - // Assume Data is loaded by this point - commit_() - _channel.state = AudioState.PLAYING - _channel.enabled = true - return - } - - if (state == AudioState.PLAYING) { - commit_() - if (finished || _stopRequested) { - _channel.state = AudioState.STOPPING - } - return - } - - if (state == AudioState.STOPPING) { - // TODO: Fade - commit_() - // if fade complete - _channel.enabled = false - if (!_channel.enabled) { - _channel.state = AudioState.STOPPED - } - return - } + foreign stop() - if (state == AudioState.STOPPING) {} - } - - commit_() { - _channel.volume = _volume - _channel.pan = _pan - _channel.loop = _loop - if (_position != null) { - _channel.position = _position - _position = null - } - } - - // Private - channel_ { _channel } - id { _id } - - // Public - position { _position || _channel.position } - position=(v) { _position = v } - length { _length } - soundId { _soundId } - volume { _volume } - volume=(volume) { _volume = volume } - loop { _loop } - loop=(loop) { _loop = loop } - pan { _pan } - pan=(pan) { _pan = pan } - state { - if (_channel != null) { - return _channel.state - } else { - return AudioState.STOPPED - } - } - finished { !_channel.enabled || state == AudioState.STOPPED } + finished { state == AudioState.STOPPED } } class AudioEngine { // TODO: Allow device enumeration and selection static init() { - __unloadQueue = [] __nameMap = {} __files = {} - __nextId = 0 __channels = {} - f_captureVariable() } - foreign static f_captureVariable() static register(name, path) { __nameMap[name] = path } + static load(name, path) { register(name, path) return load(name) @@ -178,7 +90,9 @@ class AudioEngine { } static unload(name) { - __unloadQueue.add(name) + var path = __nameMap[name] + __files.remove(path) + System.gc() } static unloadAll() { @@ -189,55 +103,19 @@ class AudioEngine { static play(name, volume) { play(name, volume, false, 0) } static play(name, volume, loop) { play(name, volume, loop, 0) } static play(name, volume, loop, pan) { - var systemChannel = SystemChannel.new(name) - systemChannel.audio = load(name) - var channel = AudioChannelFacade.wrap(__nextId, systemChannel) - __channels[__nextId] = channel + var channel = SystemChannel.new(name) + channel.audio = load(name) channel.volume = volume channel.pan = pan channel.loop = loop - - __nextId = __nextId + 1 + __channels[channel.id] = channel return channel } static stopAllChannels() { - __channels.values.each { |channel| channel.stop() } + f_stopAllChannels() } - foreign static f_update(list) - static update() { - var playing = __channels.values.where {|facade| - if (__unloadQueue.contains(facade.soundId)) { - __channels.remove(facade.id) - facade.release_() - return false - } - facade.update_() - if (facade.state == AudioState.STOPPED) { - __channels.remove(facade.id) - return false - } - return facade.state == AudioState.PLAYING || - facade.state == AudioState.STOPPING || - facade.state == AudioState.VIRTUALIZING - }.map {|facade| facade.channel_ }.toList - - f_update(playing) - - if (__unloadQueue.count > 0) { - __unloadQueue.each {|soundId| - if (__nameMap.containsKey(soundId)) { - var path = __nameMap[soundId] - if (__files.containsKey(path)) { - __files.remove(path) - } - } - } - // We have to force a gc here to release audio objects. - System.gc() - __unloadQueue = [] - } - } + foreign static f_stopAllChannels() } AudioEngine.init() diff --git a/src/modules/color.wren b/src/modules/color.wren index c471d1e2..7ebdf661 100644 --- a/src/modules/color.wren +++ b/src/modules/color.wren @@ -1,5 +1,5 @@ -import "math" for HexToNum, NumToHex, HexDigitToNum -import "dome" for StringUtils +import "math" for HexToNum, NumToHex, HexDigitToNum, Vector +import "stringUtils" for StringUtils var ShortColorDigit = Fn.new {|digit| @@ -8,63 +8,14 @@ var ShortColorDigit = Fn.new {|digit| // Digit is interpreted as written twice, e.g. #abc is equal to #aabbcc } -class Color { - construct hex(hex) { - if (hex is String) { - var offset = 0 - if (hex[0] == "#") { - offset = 1 - } +class Color is Vector { - if ((hex.bytes.count - offset) == 3 || (hex.bytes.count - offset) == 4) { - // Short color, e.g. #fff intepreted as #ffffff - _r = ShortColorDigit.call(hex.bytes[offset]) - _g = ShortColorDigit.call(hex.bytes[offset + 1]) - _b = ShortColorDigit.call(hex.bytes[offset + 2]) - _a = (hex.bytes.count - offset) == 4 ? ShortColorDigit.call(hex.bytes[offset + 3]) : 255 - } else { - _r = HexToNum.call(StringUtils.subString(hex, offset + 0, 2)) - _g = HexToNum.call(StringUtils.subString(hex, offset + 2, 2)) - _b = HexToNum.call(StringUtils.subString(hex, offset + 4, 2)) - _a = (hex.bytes.count - offset) == 8 ? HexToNum.call(StringUtils.subString(hex, offset + 6, 2)) : 255 - } - } else { - Fiber.abort("Color only supports hexcodes as strings or numbers") - } - } + static rgb(r, g, b) { Color.new(r, g, b, 255) } + static rgb(r, g, b, a) { Color.new(r, g, b, a) } - construct new(r, g, b) { - System.print("Color.new(_,_,_) is deprecated. Please use Color.rgb(_,_,_) instead.") - setrgb(r, g, b, 255) - } - construct new(r, g, b, a) { - System.print("Color.new(_,_,_,_) is deprecated. Please use Color.rgb(_,_,_,_) instead.") - setrgb(r, g, b, a) - } - - construct rgb(r, g, b) { - setrgb(r, g, b, 255) - } - construct rgb(r, g, b, a) { - setrgb(r, g, b, a) - } - construct hsv(h, s, v, a) { - setHSV(h, s, v) - _a = a - } - construct hsv(h, s, v) { - setHSV(h, s, v) - _a = 255 - } + static hsv(h, s, v) { hsv(h, s, v, 255) } + static hsv(h, s, v, a) { - setrgb(r, g, b, a) { - _r = r - _g = g - _b = b - _a = a - } - - setHSV(h, s, v) { h = h % 360 if (0 < s && s > 1) { Fiber.abort("Color component S is out of bounds") @@ -108,18 +59,72 @@ class Color { Fiber.abort("Invalid H value") } - _r = (rP + m) * 255 - _g = (gP + m) * 255 - _b = (bP + m) * 255 - _a = 255 + var r = (rP + m) * 255 + var g = (gP + m) * 255 + var b = (bP + m) * 255 + return Color.new(r, g, b, a) } - toNum { - return a << 24 | b << 16 | g << 8 | r + static hex(hex) { + if (hex is String) { + var offset = 0 + if (hex[0] == "#") { + offset = 1 + } + + var r + var g + var b + var a + + if ((hex.bytes.count - offset) == 3 || (hex.bytes.count - offset) == 4) { + // Short color, e.g. #fff intepreted as #ffffff + r = ShortColorDigit.call(hex.bytes[offset]) + g = ShortColorDigit.call(hex.bytes[offset + 1]) + b = ShortColorDigit.call(hex.bytes[offset + 2]) + a = (hex.bytes.count - offset) == 4 ? ShortColorDigit.call(hex.bytes[offset + 3]) : 255 + } else { + r = HexToNum.call(StringUtils.subString(hex, offset + 0, 2)) + g = HexToNum.call(StringUtils.subString(hex, offset + 2, 2)) + b = HexToNum.call(StringUtils.subString(hex, offset + 4, 2)) + a = (hex.bytes.count - offset) == 8 ? HexToNum.call(StringUtils.subString(hex, offset + 6, 2)) : 255 + } + return Color.new(r, g, b, a) + } else { + Fiber.abort("Color only supports hexcodes as strings or numbers") + } + } + + construct new() { + super() + } + construct new(r, g, b) { + super(r, g, b, 255) + checkRange() + } + construct new(r, g, b, a) { + super(r, g, b, a) + checkRange() + } + + checkRange() { + if (r < 0 || 255 < r) Fiber.abort("Red channel out of range") + if (g < 0 || 255 < g) Fiber.abort("Green channel out of range") + if (b < 0 || 255 < b) Fiber.abort("Blue channel out of range") + if (a < 0 || 255 < a) Fiber.abort("Alpha channel out of range") + } + + toNum { a << 24 | b << 16 | g << 8 | r } + static fromNum(v) { + var r = v & 0xFF + var g = (v >> 8) & 0xFF + var b = (v >> 16) & 0xFF + var a = (v >> 24) & 0xFF + return Color.rgb(r, g, b, a) } toString { - var nums = _r << 24 | _g << 16 | _b << 8 | _a + var nums = r << 24 | g << 16 | b << 8 | a var hexString = NumToHex.call(nums) while (hexString.count < 8) { hexString = "0%(hexString)" @@ -127,48 +132,50 @@ class Color { return "Color (#%(hexString))" } - a { _a } - r { _r } - g { _g } - b { _b } - - static none { AllColors["none"] } - static black { AllColors["black"] } - static darkblue { AllColors["darkblue"] } - static purple { AllColors["purple"] } - static darkpurple { AllColors["darkpurple"] } - static darkgreen { AllColors["darkgreen"] } - static brown { AllColors["brown"] } - static darkgray { AllColors["darkgray"] } - static lightgray { AllColors["lightgray"] } - static white { AllColors["white"] } - static red { AllColors["red"] } - static orange { AllColors["orange"] } - static yellow { AllColors["yellow"] } - static green { AllColors["green"] } - static blue { AllColors["blue"] } - static indigo { AllColors["indigo"] } - static pink { AllColors["pink"] } - static peach { AllColors["peach"] } + a { w } + r { x } + g { y } + b { z } + a=(v) { w = v } + r=(v) { x = v } + g=(v) { y = v } + b=(v) { z = v } + + static none { None } + static black { Black } + static darkblue { DarkBlue } + static purple { Purple } + static darkpurple { DarkPurple } + static darkgreen { DarkGreen } + static brown { Brown } + static darkgray { DarkGray } + static lightgray { LightGray } + static white { White } + static red { Red } + static orange { Orange } + static yellow { Yellow } + static green { Green } + static blue { Blue } + static indigo { Indigo } + static pink { Pink } + static peach { Peach } } -var AllColors = { - "black": Color.rgb(0, 0, 0), - "darkblue": Color.rgb(29, 43, 83), - "purple": Color.rgb(141, 60, 255), - "darkpurple": Color.rgb(126, 37, 83), - "darkgreen": Color.rgb(0, 135, 81), - "brown": Color.rgb(171, 82, 54), - "darkgray": Color.rgb(95, 87, 79), - "lightgray": Color.rgb(194, 195, 199), - "white": Color.rgb(255, 255, 255), - "red": Color.rgb(255, 0, 77), - "orange": Color.rgb(255, 163, 0), - "yellow": Color.rgb(255, 236, 39), - "green": Color.rgb(0, 228, 54), - "blue": Color.rgb(41, 173, 255), - "indigo": Color.rgb(131, 118, 156), - "pink": Color.rgb(255, 119, 168), - "peach": Color.rgb(255, 204, 170), - "none": Color.rgb(0, 0, 0, 0) -} +var Black = Color.rgb(0, 0, 0) +var DarkBlue = Color.rgb(29, 43, 83) +var Purple = Color.rgb(141, 60, 255) +var DarkPurple = Color.rgb(126, 37, 83) +var DarkGreen = Color.rgb(0, 135, 81) +var Brown = Color.rgb(171, 82, 54) +var DarkGray = Color.rgb(95, 87, 79) +var LightGray = Color.rgb(194, 195, 199) +var White = Color.rgb(255, 255, 255) +var Red = Color.rgb(255, 0, 77) +var Orange = Color.rgb(255, 163, 0) +var Yellow = Color.rgb(255, 236, 39) +var Green = Color.rgb(0, 228, 54) +var Blue = Color.rgb(41, 173, 255) +var Indigo = Color.rgb(131, 118, 156) +var Pink = Color.rgb(255, 119, 168) +var Peach = Color.rgb(255, 204, 170) +var None = Color.rgb(0, 0, 0, 0) diff --git a/src/modules/dome.c b/src/modules/dome.c index 7d9c2e09..87b47d46 100644 --- a/src/modules/dome.c +++ b/src/modules/dome.c @@ -33,7 +33,6 @@ STRING_UTILS_toLowercase(WrenVM* vm) { free(dest); } - internal void WINDOW_resize(WrenVM* vm) { ENGINE* engine = (ENGINE*)wrenGetUserData(vm); @@ -121,6 +120,25 @@ WINDOW_getFps(WrenVM* vm) { wrenSetSlotDouble(vm, 0, debug->avgFps); } +internal void +WINDOW_setColor(WrenVM* vm) { + ENGINE* engine = (ENGINE*)wrenGetUserData(vm); + ASSERT_SLOT_TYPE(vm, 1, NUM, "color"); + uint32_t color = (uint32_t)wrenGetSlotDouble(vm, 1); + uint8_t r, g, b; + getColorComponents(color, &r, &g, &b); + SDL_SetRenderDrawColor(engine->renderer, r, g, b, 0xFF); +} + +internal void +WINDOW_getColor(WrenVM* vm) { + ENGINE* engine = (ENGINE*)wrenGetUserData(vm); + uint8_t r, g, b, a; + SDL_GetRenderDrawColor(engine->renderer, &r, &g, &b, &a); + uint32_t color = (b << 16) | (g << 8) | r; + wrenSetSlotDouble(vm, 0, color); +} + internal void VERSION_getString(WrenVM* vm) { size_t len = 0; @@ -135,3 +153,5 @@ VERSION_getString(WrenVM* vm) { } wrenSetSlotBytes(vm, 0, version, len); } + + diff --git a/src/modules/dome.wren b/src/modules/dome.wren index a56d4a5d..3dbc25d7 100644 --- a/src/modules/dome.wren +++ b/src/modules/dome.wren @@ -1,3 +1,6 @@ +import "color" for Color +import "platform" for Platform + class Version { foreign static toString @@ -40,9 +43,6 @@ class Version { } } - - - class Process { foreign static args foreign static f_exit(n) @@ -56,6 +56,7 @@ class Process { } } + class Window { foreign static title=(value) foreign static title @@ -67,15 +68,16 @@ class Window { foreign static height foreign static fps - foreign static resize(width, height) -} - -// Private string handling methods -// Do not use for game code. -class StringUtils { - foreign static toLowercase(string) - - static subString(str, start, len) { - return str.bytes.skip(start).take(len).toList + foreign static f_color=(value) + foreign static f_color + static color=(value) { + var c = Color.black + if (value is Color) { + c = value + } + f_color = c.toNum } + static color { Color.fromNum(f_color) } + + foreign static resize(width, height) } diff --git a/src/modules/graphics.wren b/src/modules/graphics.wren index 67863e13..b502856e 100644 --- a/src/modules/graphics.wren +++ b/src/modules/graphics.wren @@ -40,15 +40,7 @@ class Canvas { clip() } foreign static f_pset(x, y, c) - static pget(x, y) { - var c = f_pget(x, y) - // return a << 24 | b << 16 | g << 8 | r - var r = c & 255 - var g = (c & 255 << 8) >> 8 - var b = (c & 255 << 16) >> 16 - var a = (c & 255 << 24) >> 24 - return Color.rgb(r, g, b, a) - } + static pget(x, y) { Color.fromNum(f_pget(x, y)) } foreign static f_pget(x, y) foreign static f_line(x1, y1, x2, y2, c, size) @@ -156,9 +148,7 @@ class Canvas { foreign static height static draw(object, x, y) { - if (object is Drawable) { - object.draw(x, y) - } + object.draw(x, y) } } diff --git a/src/modules/image.c b/src/modules/image.c index bac01e4c..fc84a57d 100644 --- a/src/modules/image.c +++ b/src/modules/image.c @@ -27,7 +27,8 @@ typedef struct { uint32_t tintColor; } DRAW_COMMAND; -DRAW_COMMAND DRAW_COMMAND_init(IMAGE* image) { +internal DRAW_COMMAND +DRAW_COMMAND_init(IMAGE* image) { DRAW_COMMAND command; command.image = image; @@ -88,8 +89,8 @@ DRAW_COMMAND_execute(ENGINE* engine, DRAW_COMMAND* commandPtr) { w = h; h = swap; } - for (int32_t j = 0; j < ceil(h); j++) { - for (int32_t i = 0; i < ceil(w); i++) { + for (int32_t j = 0; j < h; j++) { + for (int32_t i = 0; i < w; i++) { int32_t x = dest.x + i; int32_t y = dest.y + j; @@ -115,6 +116,11 @@ DRAW_COMMAND_execute(ENGINE* engine, DRAW_COMMAND* commandPtr) { v = swap; } + // Prevent out-of-bounds access + if ((src.x + u) < 0 || (src.x + u) >= image->width || (src.y + v) < 0 || (src.y + v) >= image->height) { + continue; + } + uint32_t preColor = *(pixel + (v * image->width + u)); uint32_t color = preColor; uint8_t alpha = (0xFF000000 & color) >> 24; @@ -304,17 +310,20 @@ IMAGE_draw(WrenVM* vm) { } } -void IMAGE_getWidth(WrenVM* vm) { +internal void +IMAGE_getWidth(WrenVM* vm) { IMAGE* image = (IMAGE*)wrenGetSlotForeign(vm, 0); wrenSetSlotDouble(vm, 0, image->width); } -void IMAGE_getHeight(WrenVM* vm) { +internal void +IMAGE_getHeight(WrenVM* vm) { IMAGE* image = (IMAGE*)wrenGetSlotForeign(vm, 0); wrenSetSlotDouble(vm, 0, image->height); } -void IMAGE_pset(WrenVM* vm) { +internal void +IMAGE_pset(WrenVM* vm) { ASSERT_SLOT_TYPE(vm, 0, FOREIGN, "image"); ASSERT_SLOT_TYPE(vm, 1, NUM, "x"); ASSERT_SLOT_TYPE(vm, 2, NUM, "y"); @@ -333,7 +342,9 @@ void IMAGE_pset(WrenVM* vm) { VM_ABORT(vm, "pset co-ordinates out of bounds") } } -void IMAGE_pget(WrenVM* vm) { + +internal void +IMAGE_pget(WrenVM* vm) { ASSERT_SLOT_TYPE(vm, 0, FOREIGN, "image"); ASSERT_SLOT_TYPE(vm, 1, NUM, "x"); ASSERT_SLOT_TYPE(vm, 2, NUM, "y"); diff --git a/src/modules/image.wren b/src/modules/image.wren index 77387dcd..1c5494ad 100644 --- a/src/modules/image.wren +++ b/src/modules/image.wren @@ -101,14 +101,7 @@ foreign class ImageData is Drawable { } foreign f_pget(x, y) - pget(x, y) { - var c = f_pget(x, y) - var r = c & 255 - var g = (c & 255 << 8) >> 8 - var b = (c & 255 << 16) >> 16 - var a = (c & 255 << 24) >> 24 - return Color.rgb(r, g, b, a) - } + pget(x, y) { Color.fromNum(f_pget(x, y)) } } import "color" for Color diff --git a/src/modules/input.wren b/src/modules/input.wren index 70d9d8c8..5433ee59 100644 --- a/src/modules/input.wren +++ b/src/modules/input.wren @@ -1,5 +1,5 @@ import "vector" for Vector -import "dome" for StringUtils +import "stringUtils" for StringUtils class Input { // This sets up the whole module's event loop behaviour diff --git a/src/modules/json.wren b/src/modules/json.wren index ebb6d9a3..ab6835f7 100644 --- a/src/modules/json.wren +++ b/src/modules/json.wren @@ -4,9 +4,10 @@ class JsonOptions { static nil { 0 } static escapeSlashes { 1 } static abortOnError { 2 } + static checkCircular { 4 } - static shouldAbort(options) { - return ((options & JsonOptions.abortOnError) != JsonOptions.nil) + static contains(options, option) { + return ((options & option) != JsonOptions.nil) } } @@ -79,7 +80,7 @@ class JsonStream { if (event == isError) { _error = JsonError.new(lineno, pos, error_message, true) - if (JsonOptions.shouldAbort(_options)) { + if (JsonOptions.contains(_options, JsonOptions.abortOnError)) { end() Fiber.abort("JSON error - line %(lineno) pos %(pos): %(error_message)") } @@ -134,28 +135,30 @@ class JsonStream { } } -class Json { - - static load(path, options) { - var content = FileSystem.load(path) - return Json.decode(content, options) +class JsonEncoder { + construct new(options) { + _options = options + _circularStack = JsonOptions.contains(options, JsonOptions.checkCircular) ? [] : null } - static load(path) { - return Json.load(path, JsonOptions.abortOnError) - } + isCircle(value) { _circularStack == null || !_circularStack.any { |v| Object.same(value, v) } } - static save(path, object, options) { - var content = Json.encode(object, options) - FileSystem.save(path, content) - return content + push(value) { + if (_circularStack != null) { + _circularStack.add(value) + } } - - static save(path, object) { - return Json.save(path, object, JsonOptions.abortOnError) + pop() { + if (_circularStack != null) { + _circularStack.removeAt(-1) + } } - static encode(value, options) { + encode(value) { + if (isCircle(value)) { + Fiber.abort("Circular JSON") + } + // Loosely based on https://github.com/brandly/wren-json/blob/master/json.wren if (value is Num || value is Bool || value is Null) { return value.toString @@ -165,7 +168,7 @@ class Json { // Escape special characters var substrings = [] for (char in value) { - substrings.add(JsonStream.escapechar(char, options)) + substrings.add(JsonStream.escapechar(char, _options)) } // Compile error if you use normal escaping sequence @@ -174,20 +177,46 @@ class Json { } if (value is List) { - var substrings = value.map { |item| Json.encode(item) } + push(value) + var substrings = value.map { |item| encode(item) } + pop() return "[" + substrings.join(",") + "]" } if (value is Map) { - var substrings = value.keys.map { |key| - return Json.encode(key, options) + ":" + Json.encode(value[key], options) - } + push(value) + var substrings = value.keys.map { |key| "%(encode(key)):%(encode(value[key]))" } + pop() return "{" + substrings.join(",") + "}" } // Default behaviour is to invoke the toString method return value.toString } +} + +class Json { + + static load(path, options) { + var content = FileSystem.load(path) + return Json.decode(content, options) + } + + static load(path) { + return Json.load(path, JsonOptions.abortOnError) + } + + static save(path, object, options) { + var content = Json.encode(object, options) + FileSystem.save(path, content) + return content + } + + static save(path, object) { + return Json.save(path, object, JsonOptions.abortOnError) + } + + static encode(value, options) { JsonEncoder.new(options).encode(value) } static encode(value) { return Json.encode(value, JsonOptions.abortOnError) diff --git a/src/modules/map.c b/src/modules/map.c index 8c38ace2..209e55f0 100644 --- a/src/modules/map.c +++ b/src/modules/map.c @@ -1,7 +1,13 @@ #include "modules.inc" +typedef struct CLASS_NODE { + const char* name; + WrenForeignClassMethods methods; + struct CLASS_NODE* next; +} CLASS_NODE; + typedef struct FUNCTION_NODE { - char* signature; + const char* signature; WrenForeignMethodFn fn; struct FUNCTION_NODE* next; } FUNCTION_NODE; @@ -9,6 +15,8 @@ typedef struct FUNCTION_NODE { typedef struct MODULE_NODE { const char* name; const char* source; + bool locked; + struct CLASS_NODE* classes; struct FUNCTION_NODE* functions; struct MODULE_NODE* next; } MODULE_NODE; @@ -17,17 +25,27 @@ typedef struct { MODULE_NODE* head; } MAP; +internal MODULE_NODE* MAP_getModule(MAP* map, const char* name); + +internal bool +MAP_addModule(MAP* map, const char* name, const char* source) { + + if (MAP_getModule(map, name) != NULL) { + return false; + } -internal void -MAP_addModule(MAP* map, char* name, const char* source) { MODULE_NODE* newNode = malloc(sizeof(MODULE_NODE)); newNode->source = source; newNode->name = name; newNode->functions = NULL; + newNode->classes = NULL; + newNode->locked = false; newNode->next = map->head; map->head = newNode; + + return true; } internal MODULE_NODE* @@ -51,24 +69,49 @@ MAP_getSource(MAP* map, const char* moduleName) { return NULL; } - size_t sourceLen = strlen(module->source); - char* file = calloc(sourceLen + 1, sizeof(char)); - strcpy(file, module->source); - file[sourceLen] = '\0'; + const char* file = strdup(module->source); return file; } -internal void -MAP_addFunction(MAP* map, char* moduleName, char* signature, WrenForeignMethodFn fn) { - FUNCTION_NODE* node = (FUNCTION_NODE*) malloc(sizeof(FUNCTION_NODE)); - node->signature = signature; - node->fn = fn; +internal bool +MAP_addFunction(MAP* map, const char* moduleName, const char* signature, WrenForeignMethodFn fn) { + MODULE_NODE* module = MAP_getModule(map, moduleName); + // TODO: Check for duplicate? + if (module != NULL && !module->locked) { + FUNCTION_NODE* node = (FUNCTION_NODE*) malloc(sizeof(FUNCTION_NODE)); + node->signature = signature; + node->fn = fn; + + node->next = module->functions; + module->functions = node; + return true; + } + return false; +} +internal bool +MAP_addClass(MAP* map, const char* moduleName, const char* className, WrenForeignMethodFn allocate, WrenFinalizerFn finalize) { MODULE_NODE* module = MAP_getModule(map, moduleName); - assert(module != NULL); + // TODO: Check for duplicate? + if (module != NULL && !module->locked) { + CLASS_NODE* node = (CLASS_NODE*) malloc(sizeof(CLASS_NODE)); + node->name = className; + node->methods.allocate = allocate; + node->methods.finalize = finalize; + + node->next = module->classes; + module->classes = node; + return true; + } + return false; +} - node->next = module->functions; - module->functions = node; +internal void +MAP_lockModule(MAP* map, const char* name) { + MODULE_NODE* module = MAP_getModule(map, name); + if (module != NULL) { + module->locked = true; + } } internal WrenForeignMethodFn @@ -91,6 +134,27 @@ MAP_getFunction(MAP* map, const char* moduleName, const char* signature) { return NULL; } +internal bool +MAP_getClassMethods(MAP* map, const char* moduleName, const char* className, WrenForeignClassMethods* methods) { + MODULE_NODE* module = MAP_getModule(map, moduleName); + if (module == NULL) { + // We don't have the module, but it might be built into Wren (aka Random,Meta) + return false; + } + assert(module != NULL); + + CLASS_NODE* node = module->classes; + while (node != NULL) { + if (STRINGS_EQUAL(className, node->name)) { + *methods = node->methods; + return true; + } else { + node = node->next; + } + } + return false; +} + internal void MAP_freeFunctions(FUNCTION_NODE* node) { @@ -101,6 +165,15 @@ MAP_freeFunctions(FUNCTION_NODE* node) { node = nextNode; } } +internal void +MAP_freeClasses(CLASS_NODE* node) { + CLASS_NODE* nextNode; + while (node != NULL) { + nextNode = node->next; + free(node); + node = nextNode; + } +} internal void MAP_free(MAP* map) { @@ -108,6 +181,7 @@ MAP_free(MAP* map) { MODULE_NODE* nextModule; while (module != NULL) { MAP_freeFunctions(module->functions); + MAP_freeClasses(module->classes); nextModule = module->next; free(module); module = nextModule; diff --git a/src/modules/platform.c b/src/modules/platform.c new file mode 100644 index 00000000..8b7c504c --- /dev/null +++ b/src/modules/platform.c @@ -0,0 +1,11 @@ +internal void +PLATFORM_getTime(WrenVM* vm) { + wrenEnsureSlots(vm, 1); + wrenSetSlotDouble(vm, 0, (uint32_t)time(NULL)); +} + +internal void +PLATFORM_getName(WrenVM* vm) { + wrenEnsureSlots(vm, 1); + wrenSetSlotString(vm, 0, SDL_GetPlatform()); +} diff --git a/src/modules/platform.wren b/src/modules/platform.wren new file mode 100644 index 00000000..39ce389d --- /dev/null +++ b/src/modules/platform.wren @@ -0,0 +1,4 @@ +class Platform { + foreign static time + foreign static name +} diff --git a/src/modules/plugin.c b/src/modules/plugin.c new file mode 100644 index 00000000..4dcdd679 --- /dev/null +++ b/src/modules/plugin.c @@ -0,0 +1,12 @@ +internal void +PLUGIN_load(WrenVM* vm) { + ENGINE* engine = (ENGINE*)wrenGetUserData(vm); + const char* name = wrenGetSlotString(vm, 1); + DOME_Result result = PLUGIN_COLLECTION_add(engine, name); + if (result != DOME_RESULT_SUCCESS) { + char buf[256]; + sprintf(buf, "There was a problem initialising plugin: %s", name); + VM_ABORT(vm, buf); + } +} + diff --git a/src/modules/plugin.wren b/src/modules/plugin.wren new file mode 100644 index 00000000..b5ecc99a --- /dev/null +++ b/src/modules/plugin.wren @@ -0,0 +1,18 @@ +import "dome" for Platform + +class Plugin { + static load(path) { + if (Platform.name.startsWith("Windows")) { + path = path + ".dll" + } else if (Platform.name.startsWith("Mac OS X")) { + path = path + ".dylib" + } else { + // Linux and other, as .so is a more generic format + path = path + ".so" + } + return f_load(path) + } + + foreign static f_load(path) +} + diff --git a/src/modules/random.c b/src/modules/random.c new file mode 100644 index 00000000..c6e16b9e --- /dev/null +++ b/src/modules/random.c @@ -0,0 +1,42 @@ +uint32_t BIT_NOISE1 = 0xB5297A4D; +uint32_t BIT_NOISE2 = 0x68E31DA4; +uint32_t BIT_NOISE3 = 0x1B56C4E9; +uint32_t CAP = 0xFFFFFFFF; + +uint32_t squirrel3Hash(uint32_t position, uint32_t seed) { + uint32_t mangled = position; + mangled = mangled * BIT_NOISE1; + mangled = mangled + seed; + mangled = mangled ^ (mangled >> 8); + mangled = mangled + BIT_NOISE2; + mangled = mangled ^ (mangled << 8); + mangled = mangled * BIT_NOISE3; + mangled = mangled ^ (mangled >> 8); + return mangled & CAP; +} + +void RANDOM_noise(WrenVM* vm) { + uint32_t position = wrenGetSlotDouble(vm, 1); + uint32_t seed = wrenGetSlotDouble(vm, 2); + wrenSetSlotDouble(vm, 0, squirrel3Hash(position, seed)); +} + +typedef struct { + uint32_t seed; + uint32_t state; +} RANDOM; + +void RANDOM_allocate(WrenVM* vm) { + RANDOM* rng = wrenSetSlotNewForeign(vm, 0, 0, sizeof(RANDOM)); + uint32_t seed = wrenGetSlotDouble(vm, 1); + rng->seed = seed; + rng->state = 0; +} + +void RANDOM_finalize(void* data) {} + +void RANDOM_float(WrenVM* vm) { + RANDOM* rng = wrenGetSlotForeign(vm, 0); + double result = squirrel3Hash(rng->state++, rng->seed); + wrenSetSlotDouble(vm, 0, result / CAP); +} diff --git a/src/modules/random.wren b/src/modules/random.wren new file mode 100644 index 00000000..14683bff --- /dev/null +++ b/src/modules/random.wren @@ -0,0 +1,41 @@ +import "platform" for Platform + +class Squirrel3 { + static noise(x) { noise(x, 0) } + foreign static noise(x, seed) + + static new() { Squirrel3.new(Platform.time) } + construct new(seed) {} + + foreign float() + float(end) { float() * end } + float(start, end) { start + float(end - start) } + int(end) { float(end).floor } + int(start, end) { float(start, end).floor } + + sample(list) { list[int(list.count)] } + + sample(list, count) { + if (count > list.count) { + Fiber.abort("Cannot sample more items than the list contains") + } + var newList = shuffle(list.toList) + var end = list.count - 1 + for (i in end..count) { + newList.removeAt(i) + } + return newList + } + + shuffle(list) { + var n = list.count + var j + for (i in 0...n) { + j = int(i + 1) + list.swap(j, i) + } + return list + } +} + +var Random = Squirrel3 diff --git a/src/modules/stringUtils.wren b/src/modules/stringUtils.wren new file mode 100644 index 00000000..83ffca3e --- /dev/null +++ b/src/modules/stringUtils.wren @@ -0,0 +1,9 @@ +// Private string handling methods +// Do not use for game code. +class StringUtils { + foreign static toLowercase(string) + + static subString(str, start, len) { + return str.bytes.skip(start).take(len).toList + } +} diff --git a/src/modules/vector.wren b/src/modules/vector.wren index d2c884f4..426ec940 100644 --- a/src/modules/vector.wren +++ b/src/modules/vector.wren @@ -38,13 +38,13 @@ class Vector { unit { if (length == 0) { - return Vector.new() + return type.new() } - return Vector.new(x / length, y / length, z / length, w / length) + return type.new(x / length, y / length, z / length, w / length) } perp { - return Vector.new(-y, x) + return type.new(-y, x) } dot(other) { @@ -53,7 +53,7 @@ class Vector { } cross(other) { - return Vector.new( + return type.new( y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x @@ -62,17 +62,17 @@ class Vector { + (other) { if (!(other is Vector)) Fiber.abort("Vectors can only be subtracted from other points.") - return Vector.new(x + other.x, y + other.y, z + other.z, w + other.w) + return type.new(x + other.x, y + other.y, z + other.z, w + other.w) } - (other) { if (!(other is Vector)) Fiber.abort("Vectors can only be subtracted from other points.") - return Vector.new(x - other.x, y - other.y, z - other.z, w - other.w) + return type.new(x - other.x, y - other.y, z - other.z, w - other.w) } / (other) { if (other is Num) { // Scale by other - return Vector.new(x / other, y / other, z / other, w / other) + return type.new(x / other, y / other, z / other, w / other) } else { Fiber.abort("Vectors can only be divided by scalar values.") } @@ -81,14 +81,14 @@ class Vector { * (other) { if (other is Num) { // Scale by other - return Vector.new(x * other, y * other, z * other, w * other) + return type.new(x * other, y * other, z * other, w * other) } else { Fiber.abort("Vectors can only be multiplied by scalar values.") } } - { - return Vector.new(-x, -y, -z, -w) + return type.new(-x, -y, -z, -w) } ==(other) { diff --git a/src/plugin.c b/src/plugin.c new file mode 100644 index 00000000..98fd9cd7 --- /dev/null +++ b/src/plugin.c @@ -0,0 +1,350 @@ +internal char* +PLUGIN_COLLECTION_hookName(DOME_PLUGIN_HOOK hook) { + switch (hook) { + case DOME_PLUGIN_HOOK_PRE_UPDATE: return "pre-update"; + case DOME_PLUGIN_HOOK_POST_UPDATE: return "post-update"; + case DOME_PLUGIN_HOOK_PRE_DRAW: return "pre-draw"; + case DOME_PLUGIN_HOOK_POST_DRAW: return "post-draw"; + default: return "unknown"; + } +} + +internal DOME_Result +PLUGIN_nullHook(DOME_Context ctx) { + return DOME_RESULT_SUCCESS; +} + +internal void +PLUGIN_COLLECTION_initRecord(PLUGIN_COLLECTION* plugins, size_t index) { + plugins->active[index] = false; + plugins->objectHandle[index] = NULL; + plugins->name[index] = NULL; + plugins->preUpdateHook[index] = PLUGIN_nullHook; + plugins->postUpdateHook[index] = PLUGIN_nullHook; + plugins->preDrawHook[index] = PLUGIN_nullHook; + plugins->postDrawHook[index] = PLUGIN_nullHook; +} + +internal void +PLUGIN_COLLECTION_init(ENGINE* engine) { + PLUGIN_COLLECTION plugins = engine->plugins; + plugins.max = 0; + plugins.count = 0; + assert(plugins.count <= plugins.max); + plugins.active = NULL; + plugins.name = NULL; + plugins.objectHandle = NULL; + plugins.preUpdateHook = NULL; + plugins.postUpdateHook = NULL; + plugins.preDrawHook = NULL; + plugins.postDrawHook = NULL; + for (size_t i = 0; i < plugins.max; i++) { + PLUGIN_COLLECTION_initRecord(&plugins, i); + } + engine->plugins = plugins; +} + +internal void +PLUGIN_reportHookError(ENGINE* engine, DOME_PLUGIN_HOOK hook, const char* pluginName) { + ENGINE_printLog(engine, "DOME cannot continue as the following plugin reported a problem:\n"); + ENGINE_printLog(engine, "Plugin: %s - hook: %s\n", pluginName, PLUGIN_COLLECTION_hookName(hook)); + ENGINE_printLog(engine, "Aborting.\n"); +} + +#pragma GCC diagnostic push //Save actual diagnostics state +#pragma GCC diagnostic ignored "-Wpedantic" //Disable pedantic +internal inline DOME_Plugin_Hook +acquireHook(void* handle, const char* functionName) { + return SDL_LoadFunction(handle, functionName); +} + +internal inline DOME_Plugin_Init_Hook +acquireInitHook(void* handle, const char* functionName) { + return SDL_LoadFunction(handle, functionName); +} +#pragma GCC diagnostic pop // Restore diagnostics state + + +internal void +PLUGIN_COLLECTION_free(ENGINE* engine) { + PLUGIN_COLLECTION plugins = engine->plugins; + for (size_t i = 0; i < plugins.count; i++) { + DOME_Plugin_Hook shutdownHook = acquireHook(plugins.objectHandle[i], "PLUGIN_onShutdown"); + if (shutdownHook != NULL) { + DOME_Result result = shutdownHook(engine); + if (result != DOME_RESULT_SUCCESS) { + PLUGIN_reportHookError(engine, DOME_PLUGIN_HOOK_SHUTDOWN, plugins.name[i]); + } + } + plugins.active[i] = false; + + free((void*)plugins.name[i]); + plugins.name[i] = NULL; + + SDL_UnloadObject(plugins.objectHandle[i]); + plugins.objectHandle[i] = NULL; + + plugins.preUpdateHook[i] = NULL; + plugins.postUpdateHook[i] = NULL; + plugins.preDrawHook[i] = NULL; + plugins.postDrawHook[i] = NULL; + } + + plugins.max = 0; + plugins.count = 0; + + free((void*)plugins.active); + plugins.active = NULL; + + free((void*)plugins.name); + plugins.name = NULL; + + free((void*)plugins.objectHandle); + plugins.objectHandle = NULL; + + engine->plugins = plugins; + + free((void*)plugins.preUpdateHook); + free((void*)plugins.postUpdateHook); + free((void*)plugins.preDrawHook); + free((void*)plugins.postDrawHook); +} + +internal DOME_Result +PLUGIN_COLLECTION_runHook(ENGINE* engine, DOME_PLUGIN_HOOK hook) { + PLUGIN_COLLECTION plugins = engine->plugins; + bool failure = false; + + for (size_t i = 0; i < plugins.count; i++) { + assert(plugins.active[i]); + DOME_Result result = DOME_RESULT_SUCCESS; + + switch (hook) { + case DOME_PLUGIN_HOOK_PRE_UPDATE: + { result = plugins.preUpdateHook[i](engine); } break; + case DOME_PLUGIN_HOOK_POST_UPDATE: + { result = plugins.postUpdateHook[i](engine); } break; + case DOME_PLUGIN_HOOK_PRE_DRAW: + { result = plugins.preDrawHook[i](engine); } break; + case DOME_PLUGIN_HOOK_POST_DRAW: + { result = plugins.postDrawHook[i](engine); } break; + default: break; + } + if (result != DOME_RESULT_SUCCESS) { + PLUGIN_reportHookError(engine, hook, plugins.name[i]); + failure = true; + break; + } + } + if (failure) { + return DOME_RESULT_FAILURE; + } + + return DOME_RESULT_SUCCESS; +} + +internal DOME_Result +PLUGIN_COLLECTION_add(ENGINE* engine, const char* name) { + void* handle = SDL_LoadObject(name); + if (handle == NULL) { + bool shouldFree = false; + const char* path = resolvePath(name, &shouldFree); + handle = SDL_LoadObject(path); + if (shouldFree) { + free((void*)path); + } + } + + if (handle == NULL) { + ENGINE_printLog(engine, "%s\n", SDL_GetError()); + return DOME_RESULT_FAILURE; + } + + PLUGIN_COLLECTION plugins = engine->plugins; + size_t next = plugins.count; + + if (next >= plugins.max) { + #define PLUGIN_FIELD_REALLOC(FIELD, TYPE) \ + do {\ + void* prev = plugins.FIELD; \ + plugins.FIELD = realloc(plugins.FIELD, sizeof(TYPE) * plugins.max); \ + if (plugins.FIELD == NULL) { \ + plugins.FIELD = prev; \ + ENGINE_printLog(engine, "There was a problem allocating memory for plugins\n"); \ + return DOME_RESULT_FAILURE; \ + }\ + } while(false); + + plugins.max = plugins.max == 0 ? 2 : plugins.max * 2; + + PLUGIN_FIELD_REALLOC(active, bool); + PLUGIN_FIELD_REALLOC(name, char*); + PLUGIN_FIELD_REALLOC(objectHandle, void*); + PLUGIN_FIELD_REALLOC(preUpdateHook, void*); + PLUGIN_FIELD_REALLOC(postUpdateHook, void*); + PLUGIN_FIELD_REALLOC(preDrawHook, void*); + PLUGIN_FIELD_REALLOC(postDrawHook, void*); + + if (next == 0) { + PLUGIN_COLLECTION_initRecord(&plugins, 0); + } + for (size_t i = next + 1; i < plugins.max; i++) { + PLUGIN_COLLECTION_initRecord(&plugins, i); + } + + #undef PLUGIN_FIELD_REALLOC + } + plugins.active[next] = true; + plugins.name[next] = strdup(name); + plugins.objectHandle[next] = handle; + plugins.count++; + + // Acquire hook function pointers + DOME_Plugin_Hook hook; + hook = acquireHook(handle, "PLUGIN_preUpdate"); + if (hook != NULL) { + plugins.preUpdateHook[next] = hook; + } + hook = acquireHook(handle, "PLUGIN_postUpdate"); + if (hook != NULL) { + plugins.postUpdateHook[next] = hook; + } + hook = acquireHook(handle, "PLUGIN_preDraw"); + if (hook != NULL) { + plugins.preDrawHook[next] = hook; + } + hook = acquireHook(handle, "PLUGIN_postDraw"); + if (hook != NULL) { + plugins.postDrawHook[next] = hook; + } + + engine->plugins = plugins; + + DOME_Plugin_Init_Hook initHook; + initHook = acquireInitHook(handle, "PLUGIN_onInit"); + if (initHook != NULL) { + return initHook(DOME_getAPI, engine); + } + + return DOME_RESULT_SUCCESS; +} + + +internal DOME_Result +DOME_registerModuleImpl(DOME_Context ctx, const char* name, const char* source) { + + ENGINE* engine = (ENGINE*)ctx; + MAP* moduleMap = &(engine->moduleMap); + if (MAP_addModule(moduleMap, name, source)) { + return DOME_RESULT_SUCCESS; + } + return DOME_RESULT_FAILURE; +} + +internal DOME_Result +DOME_registerFnImpl(DOME_Context ctx, const char* moduleName, const char* signature, DOME_ForeignFn method) { + + ENGINE* engine = (ENGINE*)ctx; + MAP* moduleMap = &(engine->moduleMap); + if (MAP_addFunction(moduleMap, moduleName, signature, (WrenForeignMethodFn)method)) { + return DOME_RESULT_SUCCESS; + } + + return DOME_RESULT_FAILURE; +} + +internal DOME_Result +DOME_registerClassImpl(DOME_Context ctx, const char* moduleName, const char* className, DOME_ForeignFn allocate, DOME_FinalizerFn finalize) { + + // TODO: handle null allocate ptr + ENGINE* engine = (ENGINE*)ctx; + MAP* moduleMap = &(engine->moduleMap); + if (MAP_addClass(moduleMap, moduleName, className, (WrenForeignMethodFn)allocate, (WrenFinalizerFn)finalize)) { + return DOME_RESULT_SUCCESS; + } + + return DOME_RESULT_FAILURE; +} + +internal void +DOME_lockModuleImpl(DOME_Context ctx, const char* moduleName) { + ENGINE* engine = (ENGINE*)ctx; + MAP* moduleMap = &(engine->moduleMap); + + MAP_lockModule(moduleMap, moduleName); +} + +internal DOME_Context +DOME_getVMContext(WrenVM* vm) { + return wrenGetUserData(vm); +} +internal void +DOME_printLog(DOME_Context ctx, const char* text, ...) { + va_list args; + va_start(args, text); + ENGINE_printLogVariadic(ctx, text, args); + va_end(args); +} + +WREN_API_v0 wren_v0 = { + .getUserData = wrenGetUserData, + .ensureSlots = wrenEnsureSlots, + + .getSlotType = wrenGetSlotType, + .getSlotCount = wrenGetSlotCount, + .abortFiber = wrenAbortFiber, + + .setSlotNull = wrenSetSlotNull, + .setSlotDouble = wrenSetSlotDouble, + .setSlotString = wrenSetSlotString, + .setSlotBytes = wrenSetSlotBytes, + .setSlotBool = wrenSetSlotBool, + .setSlotNewForeign = wrenSetSlotNewForeign, + .setSlotNewList = wrenSetSlotNewList, + .setSlotNewMap = wrenSetSlotNewMap, + + .getSlotBool = wrenGetSlotBool, + .getSlotDouble = wrenGetSlotDouble, + .getSlotString = wrenGetSlotString, + .getSlotBytes = wrenGetSlotBytes, + .getSlotForeign = wrenGetSlotForeign, + + .getListCount = wrenGetListCount, + .getListElement = wrenGetListElement, + .setListElement = wrenSetListElement, + .insertInList = wrenInsertInList, + + .getMapCount = wrenGetMapCount, + .getMapContainsKey = wrenGetMapContainsKey, + .getMapValue = wrenGetMapValue, + .setMapValue = wrenSetMapValue, + .removeMapValue = wrenRemoveMapValue +}; + +DOME_API_v0 dome_v0 = { + .registerModule = DOME_registerModuleImpl, + .registerFn = DOME_registerFnImpl, + .registerClass = DOME_registerClassImpl, + .lockModule = DOME_lockModuleImpl, + .getContext = DOME_getVMContext, + .log = DOME_printLog +}; + +external void* +DOME_getAPI(API_TYPE api, int version) { + if (api == API_DOME) { + if (version == 0) { + return &dome_v0; + } + } else if (api == API_WREN) { + if (version == 0) { + return &wren_v0; + } + } else if (api == API_AUDIO) { + if (version == 0) { + return &audio_v0; + } + } + + return NULL; +} diff --git a/src/plugin.h b/src/plugin.h new file mode 100644 index 00000000..6bb21ce5 --- /dev/null +++ b/src/plugin.h @@ -0,0 +1,39 @@ +#ifndef PLUGIN_H +#define PLUGIN_H + +struct ENGINE_t; + +typedef DOME_Result (*DOME_Plugin_Init_Hook) (DOME_getAPIFunction apiFn, DOME_Context context); +typedef DOME_Result (*DOME_foreignFn) (DOME_Context context, WrenVM* vm); + +typedef struct { + // Total allocated size + size_t max; + // Number in use + size_t count; + bool* active; + const char** name; + void** objectHandle; + DOME_Plugin_Hook* preUpdateHook; + DOME_Plugin_Hook* postUpdateHook; + DOME_Plugin_Hook* preDrawHook; + DOME_Plugin_Hook* postDrawHook; +} PLUGIN_COLLECTION; + +typedef enum { + DOME_PLUGIN_HOOK_INIT, + DOME_PLUGIN_HOOK_SHUTDOWN, + DOME_PLUGIN_HOOK_PRE_UPDATE, + DOME_PLUGIN_HOOK_POST_UPDATE, + DOME_PLUGIN_HOOK_PRE_DRAW, + DOME_PLUGIN_HOOK_POST_DRAW, + DOME_PLUGIN_HOOK_UNKNOWN +} DOME_PLUGIN_HOOK; + +internal void PLUGIN_COLLECTION_init(struct ENGINE_t* engine); +internal void PLUGIN_COLLECTION_free(struct ENGINE_t* engine); + +internal DOME_Result PLUGIN_COLLECTION_runHook(struct ENGINE_t* engine, DOME_PLUGIN_HOOK hook); +internal DOME_Result PLUGIN_COLLECTION_add(struct ENGINE_t* engine, const char* name); + +#endif diff --git a/src/vm.c b/src/vm.c index be6046ab..68750396 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1,52 +1,13 @@ internal WrenForeignClassMethods -VM_bind_foreign_class(WrenVM* vm, const char* module, const char* className) { +VM_bind_foreign_class(WrenVM* vm, const char* moduleName, const char* className) { WrenForeignClassMethods methods; // Assume an unknown class. methods.allocate = NULL; methods.finalize = NULL; - - if (STRINGS_EQUAL(module, "image")) { - if (STRINGS_EQUAL(className, "ImageData")) { - methods.allocate = IMAGE_allocate; - methods.finalize = IMAGE_finalize; - } else if (STRINGS_EQUAL(className, "DrawCommand")) { - methods.allocate = DRAW_COMMAND_allocate; - methods.finalize = DRAW_COMMAND_finalize; - } - } else if (STRINGS_EQUAL(module, "io")) { - if (STRINGS_EQUAL(className, "DataBuffer")) { - methods.allocate = DBUFFER_allocate; - methods.finalize = DBUFFER_finalize; - } else if (STRINGS_EQUAL(className, "AsyncOperation")) { - methods.allocate = ASYNCOP_allocate; - methods.finalize = ASYNCOP_finalize; - } - } else if (STRINGS_EQUAL(module, "audio")) { - if (STRINGS_EQUAL(className, "AudioData")) { - methods.allocate = AUDIO_allocate; - methods.finalize = AUDIO_finalize; - } else if (STRINGS_EQUAL(className, "SystemChannel")) { - methods.allocate = AUDIO_CHANNEL_allocate; - methods.finalize = AUDIO_CHANNEL_finalize; - } - } else if (STRINGS_EQUAL(module, "input")) { - if (STRINGS_EQUAL(className, "SystemGamePad")) { - methods.allocate = GAMEPAD_allocate; - methods.finalize = GAMEPAD_finalize; - } - } else if (STRINGS_EQUAL(module, "font")) { - if (STRINGS_EQUAL(className, "FontFile")) { - methods.allocate = FONT_allocate; - methods.finalize = FONT_finalize; - } else if (STRINGS_EQUAL(className, "RasterizedFont")) { - methods.allocate = FONT_RASTER_allocate; - methods.finalize = FONT_RASTER_finalize; - } - } else { - // TODO: Check if it's a module we lazy-loaded - - } + ENGINE* engine = (ENGINE*)wrenGetUserData(vm); + MAP moduleMap = engine->moduleMap; + MAP_getClassMethods(&moduleMap, moduleName, className, &methods); return methods; } @@ -119,8 +80,8 @@ VM_load_module(WrenVM* vm, const char* name) { // This pointer becomes owned by the WrenVM and freed later. char* file = ENGINE_readFile(engine, path, NULL); - free(path); + result.source = file; return result; } @@ -174,6 +135,17 @@ internal void VM_error(WrenVM* vm, WrenErrorType type, const char* module, engine->debug.errorBufLen += len; } +internal const char* +VM_resolve_module_name(WrenVM* vm, const char* importer, const char* name) { + const char* localName = name; + if (strlen(name) > 1) { + while (localName[0] == '.' && localName[1] == '/') { + localName = localName + 2; + } + } + return path_normalize(localName); +} + internal WrenVM* VM_create(ENGINE* engine) { WrenConfiguration config; wrenInitConfiguration(&config); @@ -181,6 +153,7 @@ internal WrenVM* VM_create(ENGINE* engine) { config.errorFn = VM_error; config.bindForeignMethodFn = VM_bind_foreign_method; config.bindForeignClassFn = VM_bind_foreign_class; + config.resolveModuleFn = VM_resolve_module_name; config.loadModuleFn = VM_load_module; WrenVM* vm = wrenNewVM(&config); @@ -188,8 +161,11 @@ internal WrenVM* VM_create(ENGINE* engine) { // Set modules + // StringUtils + MAP_addFunction(&engine->moduleMap, "stringUtils", "static StringUtils.toLowercase(_)", STRING_UTILS_toLowercase); + MAP_lockModule(&engine->moduleMap, "stringUtils"); + // DOME - MAP_addFunction(&engine->moduleMap, "dome", "static StringUtils.toLowercase(_)", STRING_UTILS_toLowercase); MAP_addFunction(&engine->moduleMap, "dome", "static Process.f_exit(_)", PROCESS_exit); MAP_addFunction(&engine->moduleMap, "dome", "static Process.args", PROCESS_getArguments); MAP_addFunction(&engine->moduleMap, "dome", "static Window.resize(_,_)", WINDOW_resize); @@ -202,7 +178,10 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "dome", "static Window.width", WINDOW_getWidth); MAP_addFunction(&engine->moduleMap, "dome", "static Window.height", WINDOW_getHeight); MAP_addFunction(&engine->moduleMap, "dome", "static Window.fps", WINDOW_getFps); + MAP_addFunction(&engine->moduleMap, "dome", "static Window.f_color=(_)", WINDOW_setColor); + MAP_addFunction(&engine->moduleMap, "dome", "static Window.f_color", WINDOW_getColor); MAP_addFunction(&engine->moduleMap, "dome", "static Version.toString", VERSION_getString); + MAP_lockModule(&engine->moduleMap, "dome"); // Canvas MAP_addFunction(&engine->moduleMap, "graphics", "static Canvas.f_pset(_,_,_)", CANVAS_pset); @@ -221,11 +200,17 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "graphics", "static Canvas.f_resize(_,_,_)", CANVAS_resize); MAP_addFunction(&engine->moduleMap, "graphics", "static Canvas.width", CANVAS_getWidth); MAP_addFunction(&engine->moduleMap, "graphics", "static Canvas.height", CANVAS_getHeight); + MAP_lockModule(&engine->moduleMap, "graphics"); // Font + MAP_addClass(&engine->moduleMap, "font", "FontFile", FONT_allocate, FONT_finalize); + MAP_addClass(&engine->moduleMap, "font", "RasterizedFont", FONT_RASTER_allocate, FONT_RASTER_finalize); MAP_addFunction(&engine->moduleMap, "font", "RasterizedFont.f_print(_,_,_,_)", FONT_RASTER_print); MAP_addFunction(&engine->moduleMap, "font", "RasterizedFont.antialias=(_)", FONT_RASTER_setAntiAlias); + MAP_lockModule(&engine->moduleMap, "font"); + // Image + MAP_addClass(&engine->moduleMap, "image", "ImageData", IMAGE_allocate, IMAGE_finalize); MAP_addFunction(&engine->moduleMap, "image", "ImageData.f_loadFromFile(_)", IMAGE_loadFromFile); MAP_addFunction(&engine->moduleMap, "image", "ImageData.f_getPNG()", IMAGE_getPNG); MAP_addFunction(&engine->moduleMap, "image", "ImageData.draw(_,_)", IMAGE_draw); @@ -233,11 +218,12 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "image", "ImageData.height", IMAGE_getHeight); MAP_addFunction(&engine->moduleMap, "image", "ImageData.f_pget(_,_)", IMAGE_pget); MAP_addFunction(&engine->moduleMap, "image", "ImageData.f_pset(_,_,_)", IMAGE_pset); + MAP_addClass(&engine->moduleMap, "image", "DrawCommand", DRAW_COMMAND_allocate, DRAW_COMMAND_finalize); MAP_addFunction(&engine->moduleMap, "image", "DrawCommand.draw(_,_)", DRAW_COMMAND_draw); + MAP_lockModule(&engine->moduleMap, "image"); // Audio - MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.enabled=(_)", AUDIO_CHANNEL_setEnabled); - MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.enabled", AUDIO_CHANNEL_getEnabled); + MAP_addClass(&engine->moduleMap, "audio", "SystemChannel", AUDIO_CHANNEL_allocate, AUDIO_CHANNEL_finalize); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.loop=(_)", AUDIO_CHANNEL_setLoop); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.loop", AUDIO_CHANNEL_getLoop); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.pan=(_)", AUDIO_CHANNEL_setPan); @@ -247,15 +233,19 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.position=(_)", AUDIO_CHANNEL_setPosition); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.state=(_)", AUDIO_CHANNEL_setState); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.audio=(_)", AUDIO_CHANNEL_setAudio); + MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.stop()", AUDIO_CHANNEL_stop); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.state", AUDIO_CHANNEL_getState); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.length", AUDIO_CHANNEL_getLength); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.position", AUDIO_CHANNEL_getPosition); MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.soundId", AUDIO_CHANNEL_getSoundId); + MAP_addFunction(&engine->moduleMap, "audio", "SystemChannel.id", AUDIO_CHANNEL_getId); + // AudioData + MAP_addClass(&engine->moduleMap, "audio", "AudioData", AUDIO_allocate, AUDIO_finalize); MAP_addFunction(&engine->moduleMap, "audio", "AudioData.length", AUDIO_getLength); + MAP_addFunction(&engine->moduleMap, "audio", "static AudioEngine.f_stopAllChannels()", AUDIO_ENGINE_wrenStopAll); - MAP_addFunction(&engine->moduleMap, "audio", "static AudioEngine.f_update(_)", AUDIO_ENGINE_update); - MAP_addFunction(&engine->moduleMap, "audio", "static AudioEngine.f_captureVariable()", AUDIO_ENGINE_capture); + MAP_lockModule(&engine->moduleMap, "audio"); // FileSystem MAP_addFunction(&engine->moduleMap, "io", "static FileSystem.f_load(_,_)", FILESYSTEM_loadAsync); @@ -267,18 +257,21 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "io", "static FileSystem.basePath()", FILESYSTEM_getBasePath); MAP_addFunction(&engine->moduleMap, "io", "static FileSystem.createDirectory(_)", FILESYSTEM_createDirectory); - // Buffer + MAP_addClass(&engine->moduleMap, "io", "DataBuffer", DBUFFER_allocate, DBUFFER_finalize); MAP_addFunction(&engine->moduleMap, "io", "static DataBuffer.f_capture()", DBUFFER_capture); MAP_addFunction(&engine->moduleMap, "io", "DataBuffer.f_data", DBUFFER_getData); MAP_addFunction(&engine->moduleMap, "io", "DataBuffer.ready", DBUFFER_getReady); MAP_addFunction(&engine->moduleMap, "io", "DataBuffer.f_length", DBUFFER_getLength); // AsyncOperation + MAP_addClass(&engine->moduleMap, "io", "AsyncOperation", ASYNCOP_allocate, ASYNCOP_finalize); MAP_addFunction(&engine->moduleMap, "io", "AsyncOperation.result", ASYNCOP_getResult); MAP_addFunction(&engine->moduleMap, "io", "AsyncOperation.complete", ASYNCOP_getComplete); + MAP_lockModule(&engine->moduleMap, "io"); // Input + MAP_addClass(&engine->moduleMap, "input", "SystemGamePad", GAMEPAD_allocate, GAMEPAD_finalize); MAP_addFunction(&engine->moduleMap, "input", "static Input.f_captureVariables()", INPUT_capture); MAP_addFunction(&engine->moduleMap, "input", "static Mouse.x", MOUSE_getX); MAP_addFunction(&engine->moduleMap, "input", "static Mouse.y", MOUSE_getY); @@ -296,6 +289,7 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "input", "SystemGamePad.attached", GAMEPAD_isAttached); MAP_addFunction(&engine->moduleMap, "input", "SystemGamePad.name", GAMEPAD_getName); MAP_addFunction(&engine->moduleMap, "input", "SystemGamePad.id", GAMEPAD_getId); + MAP_lockModule(&engine->moduleMap, "input"); // Json MAP_addFunction(&engine->moduleMap, "json", "JsonStream.stream_begin(_)", JSON_STREAM_begin); @@ -306,6 +300,22 @@ internal WrenVM* VM_create(ENGINE* engine) { MAP_addFunction(&engine->moduleMap, "json", "JsonStream.lineno", JSON_STREAM_lineno); MAP_addFunction(&engine->moduleMap, "json", "JsonStream.pos", JSON_STREAM_pos); MAP_addFunction(&engine->moduleMap, "json", "static JsonStream.escapechar(_,_)", JSON_STREAM_escapechar); + MAP_lockModule(&engine->moduleMap, "json"); + + // Platform + MAP_addFunction(&engine->moduleMap, "platform", "static Platform.time", PLATFORM_getTime); + MAP_addFunction(&engine->moduleMap, "platform", "static Platform.name", PLATFORM_getName); + MAP_lockModule(&engine->moduleMap, "platform"); + + // Plugin + MAP_addFunction(&engine->moduleMap, "plugin", "static Plugin.f_load(_)", PLUGIN_load); + MAP_lockModule(&engine->moduleMap, "plugin"); + + // Random + MAP_addClass(&engine->moduleMap, "random", "Squirrel3", RANDOM_allocate, RANDOM_finalize); + MAP_addFunction(&engine->moduleMap, "random", "static Squirrel3.noise(_,_)", RANDOM_noise); + MAP_addFunction(&engine->moduleMap, "random", "Squirrel3.float()", RANDOM_float); + MAP_lockModule(&engine->moduleMap, "random"); return vm; }