diff --git a/game.d b/game.d index e5f3bd3..6cefab6 100644 --- a/game.d +++ b/game.d @@ -99,11 +99,43 @@ SimpleWindow create2dWindow(string title, int width = 512, int height = 512) { glDisable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); + window.windowResized = (newWidth, newHeight) { + int x, y, w, h; + + // FIXME: this works for only square original sizes + if(newWidth < newHeight) { + w = newWidth; + h = newWidth * height / width; + x = 0; + y = (newHeight - h) / 2; + } else { + w = newHeight * width / height; + h = newHeight; + x = (newWidth - w) / 2; + y = 0; + } + + glViewport(x, y, w, h); + window.redrawOpenGlSceneNow(); + }; + return window; } -/// This is the base class for your game. -class GameHelperBase { +/++ + This is the base class for your game. + + You should destroy this explicitly. Easiest + way is to do this in your `main` function: + + --- + auto game = new MyGameSubclass(); + scope(exit) .destroy(game); + + runGame(game); + --- ++/ +abstract class GameHelperBase { /// Implement this to draw. abstract void drawFrame(); @@ -125,12 +157,10 @@ class GameHelperBase { bool wantAudio() { return false; } /// You must override [wantAudio] and return true for this to be valid; - AudioPcmOutThread audio; + AudioOutputThread audio; this() { - if(wantAudio) { - audio = new AudioPcmOutThread(); - } + audio = AudioOutputThread(wantAudio()); } protected bool redrawForced; @@ -202,6 +232,8 @@ struct VirtualController { Select, Start, L, R } + @nogc pure nothrow @safe: + /++ History: Added April 30, 2020 +/ @@ -231,23 +263,43 @@ struct VirtualController { } } -/// The max rates are given in executions per second -/// Redraw will never be called unless there has been at least one update -void runGame(T : GameHelperBase)(T game, int maxUpdateRate = 20, int maxRedrawRate = 0) { +/++ + Deprecated, use the other overload instead. + + History: + Deprecated on May 9, 2020. Instead of calling + `runGame(your_instance);` run `runGame!YourClass();` + instead. If you needed to change something in the game + ctor, make a default constructor in your class to do that + instead. ++/ +deprecated("Use runGame!YourGameType(updateRate, redrawRate); instead now.") +void runGame()(GameHelperBase game, int maxUpdateRate = 20, int maxRedrawRate = 0) { assert(0, "this overload is deprecated, use runGame!YourClass instead"); } + +/++ + Runs your game. It will construct the given class and destroy it at end of scope. + Your class must have a default constructor and must implement [GameHelperBase]. + Your class should also probably be `final` for performance reasons. + + $(TIP + If you need to pass parameters to your game class, you can define + it as a nested class in your `main` function and access the local + variables that way instead of passing them explicitly through the + constructor. + ) + + Params: + maxUpdateRate = The max rates are given in executions per second + maxRedrawRate = Redraw will never be called unless there has been at least one update ++/ +void runGame(T : GameHelperBase)(int maxUpdateRate = 20, int maxRedrawRate = 0) { + + + auto game = new T(); + scope(exit) .destroy(game); + // this is a template btw because then it can statically dispatch // the members instead of going through the virtual interface. - if(game.audio !is null) { - game.audio.start(); - } - - scope(exit) - if(game.audio !is null) { - import std.stdio; - game.audio.stop(); - game.audio.join(); - game.audio = null; - } - int joystickPlayers = enableJoystickInput(); scope(exit) closeJoysticks(); diff --git a/jsvar.d b/jsvar.d index 454cc5a..bd9b531 100644 --- a/jsvar.d +++ b/jsvar.d @@ -2239,13 +2239,13 @@ WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if( Parameters!(__traits(getOverloads, Class, memberName)[idx]) fargs; - /* enum lol = static_foreach(fargs.length, 1, -1, `__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) : ParamDefault!(__traits(getOverloads, Class, memberName)[idx], `,`)(),`,`)`); - */ + /* enum lol = static_foreach(fargs.length, 1, -1, `__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) : typeof(fargs[`,`]).init,`,`)`); + */ // FIXME: what if there are multiple @scriptable overloads?! // FIXME: what about @properties? @@ -2401,6 +2401,18 @@ WrappedNativeObject wrapNativeObject(Struct)(Struct* obj) if(is(Struct == struct return null; // FIXME } +/+ + _IDX_ + + static_foreach(T.length, q{ + mixin(q{ + void + } ~ __traits(identifier, T[_IDX_]) ~ q{ + + } + }); ++/ + private string static_foreach(size_t length, int t_start_idx, int t_end_idx, string[] t...) pure { assert(__ctfe); diff --git a/script.d b/script.d index ea18220..c836618 100644 --- a/script.d +++ b/script.d @@ -3436,6 +3436,12 @@ void repl(bool enhanced = false)(var globals) { import std.stdio; auto lines() { return stdin.byLine; } } + + bool exited; + if(globals == null) + globals = var.emptyObject; + globals.exit = () { exited = true; }; + import std.algorithm; auto variables = (globals.payloadType() == var.Type.Object && globals._payload._object !is null) ? globals._payload._object : new PrototypeObject(); @@ -3445,7 +3451,7 @@ void repl(bool enhanced = false)(var globals) { , "stdin"); auto expressions = parseScript(tokens); - while(!expressions.empty) { + while(!exited && !expressions.empty) { try { expressions.popFront; auto expression = expressions.front; diff --git a/simpleaudio.d b/simpleaudio.d index 5ce7b7f..8d2efe1 100644 --- a/simpleaudio.d +++ b/simpleaudio.d @@ -174,20 +174,120 @@ void main() { Thread.sleep(dur!"msecs"(500)); // give the last note a chance to finish } +/++ + Wraps [AudioPcmOutThreadImplementation] with RAII semantics for better + error handling and disposal than the old way. + + DO NOT USE THE `new` OPERATOR ON THIS! Just construct it inline: + + --- + auto audio = AudioOutputThread(true); + audio.beep(); + --- + + History: + Added May 9, 2020 to replace the old [AudioPcmOutThread] class + that proved pretty difficult to use correctly. ++/ +struct AudioOutputThread { + @disable this(); + + /++ + Pass `true` to enable the audio thread. Otherwise, it will + just live as a dummy mock object that you should not actually + try to use. + +/ + this(bool enable) { + if(enable) { + impl = new AudioPcmOutThreadImplementation(); + impl.refcount++; + impl.start(); + impl.waitForInitialization(); + } + } + + /// Keeps an internal refcount. + this(this) { + if(impl) + impl.refcount++; + } + + /// When the internal refcount reaches zero, it stops the audio and rejoins the thread, throwing any pending exception (yes the dtor can throw! extremely unlikely though). + ~this() { + if(impl) { + impl.refcount--; + if(impl.refcount == 0) { + impl.stop(); + impl.join(); + } + } + } + + /++ + This allows you to check `if(audio)` to see if it is enabled. + +/ + bool opCast(T : bool)() { + return impl !is null; + } + + /++ + Other methods are forwarded to the implementation of type + [AudioPcmOutThreadImplementation]. See that for more information + on what you can do. + + This opDispatch template will forward all other methods directly + to that [AudioPcmOutThreadImplementation] if this is live, otherwise + it does nothing. + +/ + template opDispatch(string name) { + static if(is(typeof(__traits(getMember, impl, name)) Params == __parameters)) + auto opDispatch(Params params) { + if(impl) + return __traits(getMember, impl, name)(params); + static if(!is(typeof(return) == void)) + return typeof(return).init; + } + else static assert(0); + } + + + /// provides automatic [arsd.jsvar] script wrapping capability. Make sure the + /// script also finishes before this goes out of scope or it may end up talking + /// to a dead object.... + auto toArsdJsvar() { + return impl; + } + + /+ + alias getImpl this; + AudioPcmOutThreadImplementation getImpl() { + assert(impl !is null); + return impl; + } + +/ + private AudioPcmOutThreadImplementation impl; +} + +/++ + Old thread implementation. I decided to deprecate it in favor of [AudioOutputThread] because + RAII semantics make it easier to get right at the usage point. See that to go forward. + + History: + Deprecated on May 9, 2020. ++/ +deprecated("Use AudioOutputThread instead.") class AudioPcmOutThread {} + import core.thread; /++ Makes an audio thread for you that you can make various sounds on and it will mix them with good enough latency for simple games. - --- - auto audio = new AudioPcmOutThread(); - audio.start(); - scope(exit) { - audio.stop(); - audio.join(); - } + DO NOT USE THIS DIRECTLY. Instead, access it through + [AudioOutputThread]. + --- + auto audio = AudioOutputThread(true); audio.beep(); // you need to keep the main program alive long enough @@ -195,14 +295,25 @@ import core.thread; Thread.sleep(1.seconds); --- +/ -final class AudioPcmOutThread : Thread { - /// - this() { +final class AudioPcmOutThreadImplementation : Thread { + private this() { this.isDaemon = true; super(&run); } + private int refcount; + + private void waitForInitialization() { + shared(AudioOutput*)* ao = cast(shared(AudioOutput*)*) &this.ao; + while(isRunning && *ao is null) { + Thread.sleep(5.msecs); + } + + if(*ao is null) + join(); // it couldn't initialize, just rethrow the exception + } + /// void pause() { if(ao) { @@ -225,6 +336,7 @@ final class AudioPcmOutThread : Thread { } /// Args in hertz and milliseconds + @scriptable void beep(int freq = 900, int dur = 150, int volume = DEFAULT_VOLUME) { Sample s; s.operation = 0; // square wave @@ -235,6 +347,7 @@ final class AudioPcmOutThread : Thread { } /// + @scriptable void noise(int dur = 150, int volume = DEFAULT_VOLUME) { Sample s; s.operation = 1; // noise @@ -245,6 +358,7 @@ final class AudioPcmOutThread : Thread { } /// + @scriptable void boop(float attack = 8, int freqBase = 500, int dur = 150, int volume = DEFAULT_VOLUME) { Sample s; s.operation = 5; // custom @@ -260,6 +374,7 @@ final class AudioPcmOutThread : Thread { } /// + @scriptable void blip(float attack = 6, int freqBase = 800, int dur = 150, int volume = DEFAULT_VOLUME) { Sample s; s.operation = 5; // custom @@ -463,6 +578,7 @@ final class AudioPcmOutThread : Thread { AudioOutput ao = AudioOutput(0); this.ao = &ao; + scope(exit) this.ao = null; auto omg = this; ao.fillData = (short[] buffer) { short[BUFFER_SIZE_SHORT] bfr; @@ -1640,3 +1756,5 @@ extern(Windows): uint mciSendStringA(in char*,char*,uint,void*); } + +private enum scriptable = "arsd_jsvar_compatible";