mirror of https://github.com/adamdruppe/arsd.git
cool stuff
This commit is contained in:
parent
3e4a0ad57b
commit
8a97353e04
76
cgi.d
76
cgi.d
|
@ -5801,7 +5801,7 @@ void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(i
|
|||
import std.traits;
|
||||
|
||||
sw: switch(calledIdx) {
|
||||
static foreach(idx, memberName; __traits(derivedMembers, Interface))
|
||||
foreach(idx, memberName; __traits(derivedMembers, Interface))
|
||||
static if(__traits(isVirtualFunction, __traits(getMember, Interface, memberName))) {
|
||||
case idx:
|
||||
assert(calledFunction == __traits(getMember, Interface, memberName).mangleof);
|
||||
|
@ -7203,7 +7203,7 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
|||
// first, check for missing arguments and initialize to defaults if necessary
|
||||
|
||||
static if(is(typeof(method) P == __parameters))
|
||||
static foreach(idx, param; P) {{
|
||||
foreach(idx, param; P) {{
|
||||
// see: mustNotBeSetFromWebParams
|
||||
static if(is(param : Cgi)) {
|
||||
static assert(!is(param == immutable));
|
||||
|
@ -7213,7 +7213,7 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
|||
cast() params[idx] = cgi.getSessionObject!D();
|
||||
} else {
|
||||
bool populated;
|
||||
static foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) {
|
||||
foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) {
|
||||
static if(is(uda == ifCalledFromWeb!func, alias func)) {
|
||||
static if(is(typeof(func(cgi))))
|
||||
params[idx] = func(cgi);
|
||||
|
@ -7279,7 +7279,7 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
|||
|
||||
// set the child member
|
||||
switch(paramName) {
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
// data member!
|
||||
case memberName:
|
||||
|
@ -7373,7 +7373,7 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
|||
|
||||
sw: switch(paramName) {
|
||||
static if(is(typeof(method) P == __parameters))
|
||||
static foreach(idx, param; P) {
|
||||
foreach(idx, param; P) {
|
||||
static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) {
|
||||
// cannot be set from the outside
|
||||
} else {
|
||||
|
@ -7424,7 +7424,7 @@ private bool mustNotBeSetFromWebParams(T, attrs...)() {
|
|||
} else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) {
|
||||
return true;
|
||||
} else {
|
||||
static foreach(uda; attrs)
|
||||
foreach(uda; attrs)
|
||||
static if(is(uda == ifCalledFromWeb!func, alias func))
|
||||
return true;
|
||||
return false;
|
||||
|
@ -7432,7 +7432,7 @@ private bool mustNotBeSetFromWebParams(T, attrs...)() {
|
|||
}
|
||||
|
||||
private bool hasIfCalledFromWeb(attrs...)() {
|
||||
static foreach(uda; attrs)
|
||||
foreach(uda; attrs)
|
||||
static if(is(uda == ifCalledFromWeb!func, alias func))
|
||||
return true;
|
||||
return false;
|
||||
|
@ -7672,7 +7672,7 @@ html", true, true);
|
|||
/// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime
|
||||
void presentSuccessfulReturnAsHtml(T : MultipleResponses!Types, Types...)(Cgi cgi, T ret) {
|
||||
bool outputted = false;
|
||||
static foreach(index, type; Types) {
|
||||
foreach(index, type; Types) {
|
||||
if(ret.contains == index) {
|
||||
assert(!outputted);
|
||||
outputted = true;
|
||||
|
@ -7787,7 +7787,7 @@ html", true, true);
|
|||
auto fieldset = div.addChild("fieldset");
|
||||
fieldset.addChild("legend", beautify(T.stringof)); // FIXME
|
||||
fieldset.addChild("input", name);
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName));
|
||||
}
|
||||
|
@ -7871,13 +7871,13 @@ html", true, true);
|
|||
//alias defaults = ParameterDefaults!method;
|
||||
|
||||
static if(is(typeof(method) P == __parameters))
|
||||
static foreach(idx, _; P) {{
|
||||
foreach(idx, _; P) {{
|
||||
|
||||
alias param = P[idx .. idx + 1];
|
||||
|
||||
static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) {
|
||||
string displayName = beautify(__traits(identifier, param));
|
||||
static foreach(attr; __traits(getAttributes, param))
|
||||
foreach(attr; __traits(getAttributes, param))
|
||||
static if(is(typeof(attr) == DisplayName))
|
||||
displayName = attr.name;
|
||||
auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param)));
|
||||
|
@ -7905,10 +7905,10 @@ html", true, true);
|
|||
//alias idents = ParameterIdentifierTuple!method;
|
||||
//alias defaults = ParameterDefaults!method;
|
||||
|
||||
static foreach(idx, memberName; __traits(derivedMembers, T)) {{
|
||||
foreach(idx, memberName; __traits(derivedMembers, T)) {{
|
||||
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||
string displayName = beautify(memberName);
|
||||
static foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName)))
|
||||
foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName)))
|
||||
static if(is(typeof(attr) == DisplayName))
|
||||
displayName = attr.name;
|
||||
form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName));
|
||||
|
@ -7930,7 +7930,7 @@ html", true, true);
|
|||
} else static if(is(T : Element)) {
|
||||
return t;
|
||||
} else static if(is(T == MultipleResponses!Types, Types...)) {
|
||||
static foreach(index, type; Types) {
|
||||
foreach(index, type; Types) {
|
||||
if(t.contains == index)
|
||||
return formatReturnValueAsHtml(t.payload[index]);
|
||||
}
|
||||
|
@ -7949,7 +7949,7 @@ html", true, true);
|
|||
auto dl = Element.make("dl");
|
||||
dl.addClass("automatic-data-display");
|
||||
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
dl.addChild("dt", memberName);
|
||||
dl.addChild("dt", formatReturnValueAsHtml(__traits(getMember, t, memberName)));
|
||||
|
@ -7964,7 +7964,7 @@ html", true, true);
|
|||
auto table = cast(Table) Element.make("table");
|
||||
table.addClass("automatic-data-display");
|
||||
string[] names;
|
||||
static foreach(idx, memberName; __traits(derivedMembers, E))
|
||||
foreach(idx, memberName; __traits(derivedMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
names ~= beautify(memberName);
|
||||
}
|
||||
|
@ -7972,7 +7972,7 @@ html", true, true);
|
|||
|
||||
foreach(l; t) {
|
||||
auto tr = table.appendRow();
|
||||
static foreach(idx, memberName; __traits(derivedMembers, E))
|
||||
foreach(idx, memberName; __traits(derivedMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
static if(memberName == "id") {
|
||||
string val = to!string(__traits(getMember, l, memberName));
|
||||
|
@ -7990,7 +7990,7 @@ html", true, true);
|
|||
auto table = cast(Table) Element.make("table");
|
||||
table.addClass("automatic-data-display");
|
||||
string[] names;
|
||||
static foreach(idx, memberName; __traits(allMembers, E))
|
||||
foreach(idx, memberName; __traits(allMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
names ~= beautify(memberName);
|
||||
}
|
||||
|
@ -7998,7 +7998,7 @@ html", true, true);
|
|||
|
||||
foreach(l; t) {
|
||||
auto tr = table.appendRow();
|
||||
static foreach(idx, memberName; __traits(allMembers, E))
|
||||
foreach(idx, memberName; __traits(allMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName)));
|
||||
}
|
||||
|
@ -8087,7 +8087,7 @@ struct MultipleResponses(T...) {
|
|||
alias findHandler = findHandler!(type, HandlersToCheck[1 .. $]);
|
||||
}
|
||||
}
|
||||
static foreach(index, type; T) {{
|
||||
foreach(index, type; T) {
|
||||
alias handler = findHandler!(type, Handlers);
|
||||
static if(is(handler == void))
|
||||
static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor");
|
||||
|
@ -8095,7 +8095,7 @@ struct MultipleResponses(T...) {
|
|||
if(index == contains)
|
||||
handler(payload[index]);
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
|
@ -8195,7 +8195,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
static if(is(typeof(T.__ctor) P == __parameters)) {
|
||||
P params;
|
||||
|
||||
static foreach(pidx, param; P) {
|
||||
foreach(pidx, param; P) {
|
||||
static if(is(param : Cgi)) {
|
||||
static assert(!is(param == immutable));
|
||||
cast() params[pidx] = cgi;
|
||||
|
@ -8205,7 +8205,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
|
||||
} else {
|
||||
static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {
|
||||
static foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
|
||||
foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
|
||||
static if(is(uda == ifCalledFromWeb!func, alias func)) {
|
||||
static if(is(typeof(func(cgi))))
|
||||
params[pidx] = func(cgi);
|
||||
|
@ -8285,9 +8285,9 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
hack ~= "/";
|
||||
|
||||
switch(hack) {
|
||||
static foreach(methodName; __traits(derivedMembers, T))
|
||||
foreach(methodName; __traits(derivedMembers, T))
|
||||
static if(methodName != "__ctor")
|
||||
static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{
|
||||
foreach(idx, overload; __traits(getOverloads, T, methodName)) {
|
||||
static if(is(typeof(overload) P == __parameters))
|
||||
static if(is(typeof(overload) R == return))
|
||||
static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export")
|
||||
|
@ -8305,7 +8305,9 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
|
||||
P params;
|
||||
|
||||
static foreach(pidx, param; P) {
|
||||
string ident;
|
||||
|
||||
foreach(pidx, param; P) {
|
||||
static if(is(param : Cgi)) {
|
||||
static assert(!is(param == immutable));
|
||||
cast() params[pidx] = cgi;
|
||||
|
@ -8314,7 +8316,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
cast() params[pidx] = cgi.getSessionObject!D();
|
||||
} else {
|
||||
static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {
|
||||
static foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
|
||||
foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {
|
||||
static if(is(uda == ifCalledFromWeb!func, alias func)) {
|
||||
static if(is(typeof(func(cgi))))
|
||||
params[pidx] = func(cgi);
|
||||
|
@ -8327,7 +8329,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) {
|
||||
params[pidx] = param.getAutomaticallyForCgi(cgi);
|
||||
} else static if(is(param == string)) {
|
||||
auto ident = nextPieceFromSlash(remainingUrl);
|
||||
ident = nextPieceFromSlash(remainingUrl);
|
||||
if(ident is null) {
|
||||
// trailing slash mandated on subresources
|
||||
cgi.setResponseLocation(cgi.pathInfo ~ "/");
|
||||
|
@ -8442,7 +8444,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi);
|
||||
static if(is(typeof(ret) == MultipleResponses!Types, Types...)) {
|
||||
var json;
|
||||
static foreach(index, type; Types) {
|
||||
foreach(index, type; Types) {
|
||||
if(ret.contains == index)
|
||||
json = ret.payload[index];
|
||||
}
|
||||
|
@ -8469,7 +8471,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
//return true;
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
case "script.js":
|
||||
cgi.setResponseContentType("text/javascript");
|
||||
cgi.gzipResponse = true;
|
||||
|
@ -8491,7 +8493,7 @@ private auto serveApiInternal(T)(string urlPrefix) {
|
|||
|
||||
string defaultFormat(alias method)() {
|
||||
bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true;
|
||||
static foreach(attr; __traits(getAttributes, method)) {
|
||||
foreach(attr; __traits(getAttributes, method)) {
|
||||
static if(is(typeof(attr) == DefaultFormat)) {
|
||||
if(nonConstConditionForWorkingAroundASpuriousDmdWarning)
|
||||
return attr.value;
|
||||
|
@ -8504,7 +8506,7 @@ string[] urlNamesForMethod(alias method)(string def) {
|
|||
auto verb = Cgi.RequestMethod.GET;
|
||||
bool foundVerb = false;
|
||||
bool foundNoun = false;
|
||||
static foreach(attr; __traits(getAttributes, method)) {
|
||||
foreach(attr; __traits(getAttributes, method)) {
|
||||
static if(is(typeof(attr) == Cgi.RequestMethod)) {
|
||||
verb = attr;
|
||||
if(foundVerb)
|
||||
|
@ -8883,7 +8885,7 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
|
|||
div.addClass("Dclass_" ~ T.stringof);
|
||||
div.dataset.url = urlId;
|
||||
bool first = true;
|
||||
static foreach(idx, memberName; __traits(derivedMembers, T))
|
||||
foreach(idx, memberName; __traits(derivedMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||
if(!first) div.addChild("br"); else first = false;
|
||||
div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName)));
|
||||
|
@ -8893,7 +8895,7 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
|
|||
obj.toJsonFromReflection = delegate(t) {
|
||||
import arsd.jsvar;
|
||||
var v = var.emptyObject();
|
||||
static foreach(idx, memberName; __traits(derivedMembers, T))
|
||||
foreach(idx, memberName; __traits(derivedMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||
v[memberName] = __traits(getMember, obj, memberName);
|
||||
}
|
||||
|
@ -8927,7 +8929,7 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
|
|||
|
||||
|
||||
static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) {
|
||||
static foreach(idx, memberName; __traits(derivedMembers, Obj))
|
||||
foreach(idx, memberName; __traits(derivedMembers, Obj))
|
||||
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||
__traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName));
|
||||
}
|
||||
|
@ -8996,7 +8998,7 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
|
|||
var json = var.emptyArray;
|
||||
foreach(r; results.results) {
|
||||
var o = var.emptyObject;
|
||||
static foreach(idx, memberName; __traits(derivedMembers, typeof(r)))
|
||||
foreach(idx, memberName; __traits(derivedMembers, typeof(r)))
|
||||
static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) {
|
||||
o[memberName] = __traits(getMember, r, memberName);
|
||||
}
|
||||
|
@ -9358,7 +9360,7 @@ template dispatcher(definitions...) {
|
|||
|
||||
bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) {
|
||||
// I can prolly make this more efficient later but meh.
|
||||
static foreach(definition; definitions) {
|
||||
foreach(definition; definitions) {
|
||||
if(definition.rejectFurther) {
|
||||
if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) {
|
||||
auto ret = definition.handler(
|
||||
|
|
2
color.d
2
color.d
|
@ -1487,6 +1487,8 @@ struct Point {
|
|||
struct Size {
|
||||
int width; ///
|
||||
int height; ///
|
||||
|
||||
int area() pure nothrow @safe const @nogc { return width * height; }
|
||||
}
|
||||
|
||||
///
|
||||
|
|
890
gamehelpers.d
890
gamehelpers.d
|
@ -1,525 +1,28 @@
|
|||
// WORK IN PROGRESS
|
||||
|
||||
// register cheat code? or even a fighting game combo..
|
||||
|
||||
/++
|
||||
An add-on for simpledisplay.d, joystick.d, and simpleaudio.d
|
||||
that includes helper functions for writing simple games (and perhaps
|
||||
other multimedia programs). Whereas simpledisplay works with
|
||||
an event-driven framework, gamehelpers always uses a consistent
|
||||
timer for updates.
|
||||
Note: much of the functionality of gamehelpers was moved to [arsd.game] on May 3, 2020.
|
||||
If you used that code, change `import arsd.gamehelpers;` to `import arsd.game;` and add
|
||||
game.d to your build (in addition to gamehelpers.d; the new game.d still imports this module)
|
||||
and you should be good to go.
|
||||
|
||||
Usage example:
|
||||
This module now builds on only [arsd.color] to provide additional algorithm functions
|
||||
and data types that are common in games.
|
||||
|
||||
---
|
||||
final class MyGame : GameHelperBase {
|
||||
/// Called when it is time to redraw the frame
|
||||
/// it will try for a particular FPS
|
||||
override void drawFrame() {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
|
||||
|
||||
glLoadIdentity();
|
||||
|
||||
glColor3f(1.0, 1.0, 1.0);
|
||||
glTranslatef(x, y, 0);
|
||||
glBegin(GL_QUADS);
|
||||
|
||||
glVertex2i(0, 0);
|
||||
glVertex2i(16, 0);
|
||||
glVertex2i(16, 16);
|
||||
glVertex2i(0, 16);
|
||||
|
||||
glEnd();
|
||||
}
|
||||
|
||||
int x, y;
|
||||
override bool update(Duration deltaTime) {
|
||||
x += 1;
|
||||
y += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
override SimpleWindow getWindow() {
|
||||
auto window = create2dWindow("My game");
|
||||
// load textures and such here
|
||||
return window;
|
||||
}
|
||||
|
||||
final void fillAudioBuffer(short[] buffer) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
auto game = new MyGame();
|
||||
|
||||
runGame(game, maxRedrawRate, maxUpdateRate);
|
||||
}
|
||||
---
|
||||
|
||||
It provides an audio thread, input scaffold, and helper functions.
|
||||
|
||||
|
||||
The MyGame handler is actually a template, so you don't have virtual
|
||||
function indirection and not all functions are required. The interfaces
|
||||
are just to help you get the signatures right, they don't force virtual
|
||||
dispatch at runtime.
|
||||
History:
|
||||
Massive change on May 3, 2020 to move the previous flagship class out and to
|
||||
a new module, [arsd.game], to make this one lighter on dependencies, just
|
||||
containing helpers rather than a consolidated omnibus import.
|
||||
+/
|
||||
module arsd.gamehelpers;
|
||||
|
||||
public import arsd.color;
|
||||
public import arsd.simpledisplay;
|
||||
public import arsd.simpleaudio;
|
||||
deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
|
||||
void* create2dWindow(string title, int width = 512, int height = 512) { return null; }
|
||||
|
||||
deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
|
||||
class GameHelperBase {}
|
||||
|
||||
import std.math;
|
||||
public import core.time;
|
||||
|
||||
public import arsd.joystick;
|
||||
|
||||
SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
|
||||
auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
|
||||
|
||||
window.setAsCurrentOpenGlContext();
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glClearColor(0,0,0,0);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glOrtho(0, width, height, 0, 0, 1);
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadIdentity();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// This is the base class for your game.
|
||||
class GameHelperBase {
|
||||
/// Implement this to draw.
|
||||
abstract void drawFrame();
|
||||
|
||||
/// Implement this to update. The deltaTime tells how much real time has passed since the last update.
|
||||
/// Returns true if anything changed, which will queue up a redraw
|
||||
abstract bool update(Duration deltaTime);
|
||||
//abstract void fillAudioBuffer(short[] buffer);
|
||||
|
||||
/// Returns the main game window. This function will only be
|
||||
/// called once if you use runGame. You should return a window
|
||||
/// here like one created with `create2dWindow`.
|
||||
abstract SimpleWindow getWindow();
|
||||
|
||||
/// Override this and return true to initialize the audio system.
|
||||
/// Note that trying to use the [audio] member without this will segfault!
|
||||
bool wantAudio() { return false; }
|
||||
|
||||
/// You must override [wantAudio] and return true for this to be valid;
|
||||
AudioPcmOutThread audio;
|
||||
|
||||
this() {
|
||||
if(wantAudio) {
|
||||
audio = new AudioPcmOutThread();
|
||||
}
|
||||
}
|
||||
|
||||
protected bool redrawForced;
|
||||
|
||||
/// Forces a redraw even if update returns false
|
||||
final public void forceRedraw() {
|
||||
redrawForced = true;
|
||||
}
|
||||
|
||||
/// These functions help you handle user input. It offers polling functions for
|
||||
/// keyboard, mouse, joystick, and virtual controller input.
|
||||
///
|
||||
/// The virtual digital controllers are best to use if that model fits you because it
|
||||
/// works with several kinds of controllers as well as keyboards.
|
||||
|
||||
JoystickUpdate[4] joysticks;
|
||||
ref JoystickUpdate joystick1() { return joysticks[0]; }
|
||||
|
||||
bool[256] keyboardState;
|
||||
|
||||
VirtualController snes;
|
||||
}
|
||||
|
||||
/++
|
||||
The virtual controller is based on the SNES. If you need more detail, try using
|
||||
the joystick or keyboard and mouse members directly.
|
||||
|
||||
```
|
||||
l r
|
||||
|
||||
U X
|
||||
L R s S Y A
|
||||
D B
|
||||
```
|
||||
|
||||
For Playstation and XBox controllers plugged into the computer,
|
||||
it picks those buttons based on similar layout on the physical device.
|
||||
|
||||
For keyboard control, arrows and WASD are mapped to the d-pad (ULRD in the diagram),
|
||||
Q and E are mapped to the shoulder buttons (l and r in the diagram).So are U and P.
|
||||
|
||||
Z, X, C, V (for when right hand is on arrows) and K,L,I,O (for left hand on WASD) are mapped to B,A,Y,X buttons.
|
||||
|
||||
G is mapped to select (s), and H is mapped to start (S).
|
||||
|
||||
The space bar and enter keys are also set to button A, with shift mapped to button B.
|
||||
|
||||
|
||||
Only player 1 is mapped to the keyboard.
|
||||
+/
|
||||
struct VirtualController {
|
||||
ushort state;
|
||||
|
||||
enum Button {
|
||||
Up, Left, Right, Down,
|
||||
X, A, B, Y,
|
||||
Select, Start, L, R
|
||||
}
|
||||
|
||||
bool opIndex(Button idx) {
|
||||
return (state & (1 << (cast(int) idx))) ? true : false;
|
||||
}
|
||||
private void opIndexAssign(bool value, Button idx) {
|
||||
if(value)
|
||||
state |= (1 << (cast(int) idx));
|
||||
else
|
||||
state &= ~(1 << (cast(int) idx));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
// 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();
|
||||
|
||||
auto window = game.getWindow();
|
||||
|
||||
window.redrawOpenGlScene = &game.drawFrame;
|
||||
|
||||
auto lastUpdate = MonoTime.currTime;
|
||||
|
||||
window.eventLoop(1000 / maxUpdateRate,
|
||||
delegate() {
|
||||
foreach(p; 0 .. joystickPlayers) {
|
||||
version(linux)
|
||||
readJoystickEvents(joystickFds[p]);
|
||||
auto update = getJoystickUpdate(p);
|
||||
|
||||
if(p == 0) {
|
||||
static if(__traits(isSame, Button, PS1Buttons)) {
|
||||
// PS1 style joystick mapping compiled in
|
||||
with(Button) with(VirtualController.Button) {
|
||||
if(update.buttonWasJustPressed(square)) game.snes[Y] = true;
|
||||
if(update.buttonWasJustPressed(triangle)) game.snes[X] = true;
|
||||
if(update.buttonWasJustPressed(cross)) game.snes[B] = true;
|
||||
if(update.buttonWasJustPressed(circle)) game.snes[A] = true;
|
||||
if(update.buttonWasJustPressed(select)) game.snes[Select] = true;
|
||||
if(update.buttonWasJustPressed(start)) game.snes[Start] = true;
|
||||
if(update.buttonWasJustPressed(l1)) game.snes[L] = true;
|
||||
if(update.buttonWasJustPressed(r1)) game.snes[R] = true;
|
||||
// note: no need to check analog stick here cuz joystick.d already does it for us (per old playstation tradition)
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < -8) game.snes[Left] = true;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > 8) game.snes[Right] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < -8) game.snes[Up] = true;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > 8) game.snes[Down] = true;
|
||||
|
||||
if(update.buttonWasJustReleased(square)) game.snes[Y] = false;
|
||||
if(update.buttonWasJustReleased(triangle)) game.snes[X] = false;
|
||||
if(update.buttonWasJustReleased(cross)) game.snes[B] = false;
|
||||
if(update.buttonWasJustReleased(circle)) game.snes[A] = false;
|
||||
if(update.buttonWasJustReleased(select)) game.snes[Select] = false;
|
||||
if(update.buttonWasJustReleased(start)) game.snes[Start] = false;
|
||||
if(update.buttonWasJustReleased(l1)) game.snes[L] = false;
|
||||
if(update.buttonWasJustReleased(r1)) game.snes[R] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) > 0 && update.axisPosition(Axis.horizontalDpad) > -8) game.snes[Left] = false;
|
||||
if(update.axisChange(Axis.horizontalDpad) < 0 && update.axisPosition(Axis.horizontalDpad) < 8) game.snes[Right] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) > 0 && update.axisPosition(Axis.verticalDpad) > -8) game.snes[Up] = false;
|
||||
if(update.axisChange(Axis.verticalDpad) < 0 && update.axisPosition(Axis.verticalDpad) < 8) game.snes[Down] = false;
|
||||
|
||||
}
|
||||
|
||||
} else static if(__traits(isSame, Button, XBox360Buttons)) {
|
||||
static assert(0);
|
||||
// XBox style mapping
|
||||
// the reason this exists is if the programmer wants to use the xbox details, but
|
||||
// might also want the basic controller in here. joystick.d already does translations
|
||||
// so an xbox controller with the default build actually uses the PS1 branch above.
|
||||
/+
|
||||
case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
|
||||
case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
|
||||
case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
|
||||
case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
|
||||
|
||||
case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
|
||||
case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
|
||||
|
||||
case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
|
||||
case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
|
||||
+/
|
||||
}
|
||||
}
|
||||
|
||||
game.joysticks[p] = update;
|
||||
}
|
||||
|
||||
auto now = MonoTime.currTime;
|
||||
bool changed = game.update(now - lastUpdate);
|
||||
lastUpdate = now;
|
||||
|
||||
if(game.redrawForced) {
|
||||
changed = true;
|
||||
game.redrawForced = false;
|
||||
}
|
||||
|
||||
// FIXME: rate limiting
|
||||
if(changed)
|
||||
window.redrawOpenGlSceneNow();
|
||||
},
|
||||
|
||||
delegate (KeyEvent ke) {
|
||||
game.keyboardState[ke.hardwareCode] = ke.pressed;
|
||||
|
||||
with(VirtualController.Button)
|
||||
switch(ke.key) {
|
||||
case Key.Up, Key.W: game.snes[Up] = ke.pressed; break;
|
||||
case Key.Down, Key.S: game.snes[Down] = ke.pressed; break;
|
||||
case Key.Left, Key.A: game.snes[Left] = ke.pressed; break;
|
||||
case Key.Right, Key.D: game.snes[Right] = ke.pressed; break;
|
||||
case Key.Q, Key.U: game.snes[L] = ke.pressed; break;
|
||||
case Key.E, Key.P: game.snes[R] = ke.pressed; break;
|
||||
case Key.Z, Key.K: game.snes[B] = ke.pressed; break;
|
||||
case Key.Space, Key.Enter, Key.X, Key.L: game.snes[A] = ke.pressed; break;
|
||||
case Key.C, Key.I: game.snes[Y] = ke.pressed; break;
|
||||
case Key.V, Key.O: game.snes[X] = ke.pressed; break;
|
||||
case Key.G: game.snes[Select] = ke.pressed; break;
|
||||
case Key.H: game.snes[Start] = ke.pressed; break;
|
||||
case Key.Shift, Key.Shift_r: game.snes[B] = ke.pressed; break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/++
|
||||
Simple class for putting a TrueColorImage in as an OpenGL texture.
|
||||
|
||||
Doesn't do mipmapping btw.
|
||||
+/
|
||||
final class OpenGlTexture {
|
||||
private uint _tex;
|
||||
private int _width;
|
||||
private int _height;
|
||||
private float _texCoordWidth;
|
||||
private float _texCoordHeight;
|
||||
|
||||
/// Calls glBindTexture
|
||||
void bind() {
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
}
|
||||
|
||||
/// For easy 2d drawing of it
|
||||
void draw(Point where, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
draw(where.x, where.y, width, height, rotation, bg);
|
||||
}
|
||||
|
||||
///
|
||||
void draw(float x, float y, int width = 0, int height = 0, float rotation = 0.0, Color bg = Color.white) {
|
||||
glPushMatrix();
|
||||
glTranslatef(x, y, 0);
|
||||
|
||||
if(width == 0)
|
||||
width = this.originalImageWidth;
|
||||
if(height == 0)
|
||||
height = this.originalImageHeight;
|
||||
|
||||
glTranslatef(cast(float) width / 2, cast(float) height / 2, 0);
|
||||
glRotatef(rotation, 0, 0, 1);
|
||||
glTranslatef(cast(float) -width / 2, cast(float) -height / 2, 0);
|
||||
|
||||
glColor4f(cast(float)bg.r/255.0, cast(float)bg.g/255.0, cast(float)bg.b/255.0, cast(float)bg.a / 255.0);
|
||||
glBindTexture(GL_TEXTURE_2D, _tex);
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0, 0); glVertex2i(0, 0);
|
||||
glTexCoord2f(texCoordWidth, 0); glVertex2i(width, 0);
|
||||
glTexCoord2f(texCoordWidth, texCoordHeight); glVertex2i(width, height);
|
||||
glTexCoord2f(0, texCoordHeight); glVertex2i(0, height);
|
||||
glEnd();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
/// Use for glTexCoord2f
|
||||
float texCoordWidth() { return _texCoordWidth; }
|
||||
float texCoordHeight() { return _texCoordHeight; } /// ditto
|
||||
|
||||
/// Returns the texture ID
|
||||
uint tex() { return _tex; }
|
||||
|
||||
/// Returns the size of the image
|
||||
int originalImageWidth() { return _width; }
|
||||
int originalImageHeight() { return _height; } /// ditto
|
||||
|
||||
// explicitly undocumented, i might remove this
|
||||
TrueColorImage from;
|
||||
|
||||
/// Make a texture from an image.
|
||||
this(TrueColorImage from) {
|
||||
bindFrom(from);
|
||||
}
|
||||
|
||||
/// Generates from text. Requires ttf.d
|
||||
/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types)
|
||||
this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
bindFrom(font, size, text);
|
||||
}
|
||||
|
||||
/// Creates an empty texture class for you to use with [bindFrom] later
|
||||
/// Using it when not bound is undefined behavior.
|
||||
this() {}
|
||||
|
||||
|
||||
|
||||
/// After you delete it with dispose, you may rebind it to something else with this.
|
||||
void bindFrom(TrueColorImage from) {
|
||||
assert(from !is null);
|
||||
assert(from.width > 0 && from.height > 0);
|
||||
|
||||
import core.stdc.stdlib;
|
||||
|
||||
_width = from.width;
|
||||
_height = from.height;
|
||||
|
||||
this.from = from;
|
||||
|
||||
auto _texWidth = _width;
|
||||
auto _texHeight = _height;
|
||||
|
||||
const(ubyte)* data = from.imageData.bytes.ptr;
|
||||
bool freeRequired = false;
|
||||
|
||||
// gotta round them to the nearest power of two which means padding the image
|
||||
if((_texWidth & (_texWidth - 1)) || (_texHeight & (_texHeight - 1))) {
|
||||
_texWidth = nextPowerOfTwo(_texWidth);
|
||||
_texHeight = nextPowerOfTwo(_texHeight);
|
||||
|
||||
auto n = cast(ubyte*) malloc(_texWidth * _texHeight * 4);
|
||||
if(n is null) assert(0);
|
||||
scope(failure) free(n);
|
||||
|
||||
auto size = from.width * 4;
|
||||
auto advance = _texWidth * 4;
|
||||
int at = 0;
|
||||
int at2 = 0;
|
||||
foreach(y; 0 .. from.height) {
|
||||
n[at .. at + size] = from.imageData.bytes[at2 .. at2+ size];
|
||||
at += advance;
|
||||
at2 += size;
|
||||
}
|
||||
|
||||
data = n;
|
||||
freeRequired = true;
|
||||
|
||||
// the rest of data will be initialized to zeros automatically which is fine.
|
||||
}
|
||||
|
||||
glGenTextures(1, &_tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA,
|
||||
_texWidth, // needs to be power of 2
|
||||
_texHeight,
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
|
||||
assert(!glGetError());
|
||||
|
||||
_texCoordWidth = cast(float) _width / _texWidth;
|
||||
_texCoordHeight = cast(float) _height / _texHeight;
|
||||
|
||||
if(freeRequired)
|
||||
free(cast(void*) data);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
|
||||
assert(font !is null);
|
||||
int width, height;
|
||||
auto data = font.renderString(text, size, width, height);
|
||||
auto image = new TrueColorImage(width, height);
|
||||
int pos = 0;
|
||||
foreach(y; 0 .. height)
|
||||
foreach(x; 0 .. width) {
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = 255;
|
||||
image.imageData.bytes[pos++] = data[0];
|
||||
data = data[1 .. $];
|
||||
}
|
||||
assert(data.length == 0);
|
||||
|
||||
bindFrom(image);
|
||||
}
|
||||
|
||||
/// Deletes the texture. Using it after calling this is undefined behavior
|
||||
void dispose() {
|
||||
glDeleteTextures(1, &_tex);
|
||||
_tex = 0;
|
||||
}
|
||||
|
||||
~this() {
|
||||
if(_tex > 0)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
FIXME: i want to do stbtt_GetBakedQuad for ASCII and use that
|
||||
for simple cases especially numbers. for other stuff you can
|
||||
create the texture for the text above.
|
||||
+/
|
||||
|
||||
///
|
||||
void clearOpenGlScreen(SimpleWindow window) {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
|
||||
}
|
||||
|
||||
import arsd.color;
|
||||
|
||||
// Some math helpers
|
||||
|
||||
|
@ -534,6 +37,9 @@ int nextPowerOfTwo(int v) {
|
|||
return v;
|
||||
}
|
||||
|
||||
/++
|
||||
Calculates the cross product of <u1, u2, u3> and <v1, v2, v3>, putting the result in <s1, s2, s3>.
|
||||
+/
|
||||
void crossProduct(
|
||||
float u1, float u2, float u3,
|
||||
float v1, float v2, float v3,
|
||||
|
@ -544,6 +50,11 @@ void crossProduct(
|
|||
s3 = u1 * v2 - u2 * v1;
|
||||
}
|
||||
|
||||
/++
|
||||
3D rotates (x, y, z) theta radians about the axis represented by unit-vector (u, v, w), putting the results in (s1, s2, s3).
|
||||
|
||||
For example, to rotate about the Y axis, pass (0, 1, 0) as (u, v, w).
|
||||
+/
|
||||
void rotateAboutAxis(
|
||||
float theta, // in RADIANS
|
||||
float x, float y, float z,
|
||||
|
@ -555,6 +66,9 @@ void rotateAboutAxis(
|
|||
zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta);
|
||||
}
|
||||
|
||||
/++
|
||||
2D rotates (rotatingX, rotatingY) theta radians about (originX, originY), putting the result in (xp, yp).
|
||||
+/
|
||||
void rotateAboutPoint(
|
||||
float theta, // in RADIANS
|
||||
float originX, float originY,
|
||||
|
@ -579,3 +93,353 @@ void rotateAboutPoint(
|
|||
xp = x + originX;
|
||||
yp = y + originY;
|
||||
}
|
||||
|
||||
/++
|
||||
Represents the four basic directions on a grid. You can conveniently use it like:
|
||||
|
||||
---
|
||||
Point pt = Point(5, 3);
|
||||
pt += Dir.N; // moves up
|
||||
---
|
||||
|
||||
The opposite direction btw can be gotten with `pt * -1`.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
enum Dir { N = Point(0, -1), S = Point(0, 1), W = Point(-1, 0), E = Point(1, 0) }
|
||||
|
||||
/++
|
||||
The four directions as a static array so you can assign to a local variable
|
||||
then shuffle, etc.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Point[4] directions() {
|
||||
with(Dir) return [N, S, W, E];
|
||||
}
|
||||
|
||||
/++
|
||||
A random value off [Dir].
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Point randomDirection() {
|
||||
import std.random;
|
||||
return directions()[uniform(0, 4)];
|
||||
}
|
||||
|
||||
/++
|
||||
Cycles through all the directions the given number of times. If you have
|
||||
one cycle, it goes through each direction once in a random order. With two
|
||||
cycles, it will move each direction twice, but in random order - can be
|
||||
W, W, N, E, S, S, N, E, for example; it will not do the cycles in order but
|
||||
upon completion will have gone through them all.
|
||||
|
||||
This can be convenient because if the character's movement is not constrained,
|
||||
it will always return back to where it started after a random movement.
|
||||
|
||||
Returns: an input range of [Point]s. Please note that the current version returns
|
||||
`Point[]`, but I reserve the right to change that in the future; I only promise
|
||||
input range capabilities.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
auto randomDirectionCycle(int cycleCount = 1) {
|
||||
Point[] all = new Point[](cycleCount * 4);
|
||||
foreach(c; 0 .. cycleCount)
|
||||
all[c * 4 .. c * 4 + 4] = directions()[];
|
||||
|
||||
import std.random;
|
||||
return all.randomShuffle;
|
||||
}
|
||||
|
||||
/++
|
||||
Represents a 2d grid like an array. To encapsulate the whole `[y*width + x]` thing.
|
||||
|
||||
History:
|
||||
Added May 3, 2020
|
||||
+/
|
||||
struct Grid(T) {
|
||||
private Size size_;
|
||||
private T[] array;
|
||||
|
||||
pure @safe nothrow:
|
||||
|
||||
/// Creates a new GC-backed array
|
||||
this(Size size) {
|
||||
this.size_ = size;
|
||||
array = new T[](size.area);
|
||||
}
|
||||
|
||||
/// ditto
|
||||
this(int width, int height) {
|
||||
this(Size(width, height));
|
||||
}
|
||||
|
||||
@nogc:
|
||||
|
||||
/// Wraps an existing array.
|
||||
this(T[] array, Size size) {
|
||||
assert(array.length == size.area);
|
||||
this.array = array;
|
||||
this.size_ = size;
|
||||
}
|
||||
|
||||
@property {
|
||||
///
|
||||
inout(Size) size() inout { return size_; }
|
||||
///
|
||||
int width() const { return size.width; }
|
||||
///
|
||||
int height() const { return size.height; }
|
||||
}
|
||||
|
||||
/// Slice operation gives a view into the underlying 1d array.
|
||||
inout(T)[] opIndex() inout {
|
||||
return array;
|
||||
}
|
||||
|
||||
///
|
||||
ref inout(T) opIndex(int x, int y) inout {
|
||||
return array[y * width + x];
|
||||
}
|
||||
|
||||
///
|
||||
ref inout(T) opIndex(const Point pt) inout {
|
||||
return this.opIndex(pt.x, pt.y);
|
||||
}
|
||||
// T[] opSlice
|
||||
|
||||
///
|
||||
bool inBounds(int x, int y) const {
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
}
|
||||
|
||||
///
|
||||
bool inBounds(const Point pt) const {
|
||||
return inBounds(pt.x, pt.y);
|
||||
}
|
||||
|
||||
/// Supports `if(point in grid) {}`
|
||||
bool opBinaryRight(string op : "in")(Point pt) const {
|
||||
return inBounds(pt);
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Directions as a maskable bit flag.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
enum DirFlag : ubyte {
|
||||
N = 4,
|
||||
S = 8,
|
||||
W = 1,
|
||||
E = 2
|
||||
}
|
||||
|
||||
/++
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
DirFlag dirFlag(Dir dir) {
|
||||
assert(dir.x >= -1 && dir.x <= 1);
|
||||
assert(dir.y >= -1 && dir.y <= 1);
|
||||
|
||||
|
||||
/+
|
||||
(-1 + 3) / 2 = 2 / 2 = 1
|
||||
(1 + 3) / 2 = 4 / 2 = 2
|
||||
|
||||
So the al-gore-rhythm is
|
||||
(x + 3) / 2
|
||||
which is aka >> 1
|
||||
or
|
||||
((y + 3) / 2) << 2
|
||||
which is aka >> 1 << 2 aka << 1
|
||||
So:
|
||||
1 = left
|
||||
2 = right
|
||||
4 = up
|
||||
8 = down
|
||||
+/
|
||||
|
||||
ubyte dirFlag;
|
||||
if(dir.x) dirFlag |= ((dir.x + 3) >> 1);
|
||||
if(dir.y) dirFlag |= ((dir.y + 3) << 1);
|
||||
return cast(DirFlag) dirFlag;
|
||||
}
|
||||
|
||||
// this is public but like i don't want do document it since it can so easily fail the asserts.
|
||||
DirFlag dirFlag(Point dir) {
|
||||
return dirFlag(cast(Dir) dir);
|
||||
}
|
||||
|
||||
/++
|
||||
Generates a maze.
|
||||
|
||||
The returned array is a grid of rooms, with a bit flag pattern of directions you can travel from each room. See [DirFlag] for bits.
|
||||
|
||||
History: Added May 3, 2020
|
||||
+/
|
||||
Grid!ubyte generateMaze(int mazeWidth, int mazeHeight) {
|
||||
import std.random;
|
||||
|
||||
Point[] cells;
|
||||
cells ~= Point(uniform(0, mazeWidth), uniform(0, mazeHeight));
|
||||
|
||||
auto grid = Grid!ubyte(mazeWidth, mazeHeight);
|
||||
|
||||
Point[4] directions = .directions;
|
||||
|
||||
while(cells.length) {
|
||||
auto index = cells.length - 1; // could also be 0 or uniform or whatever too
|
||||
Point p = cells[index];
|
||||
bool added;
|
||||
foreach(dir; directions[].randomShuffle) {
|
||||
auto n = p + dir;
|
||||
if(n !in grid)
|
||||
continue;
|
||||
|
||||
if(grid[n])
|
||||
continue;
|
||||
|
||||
grid[p] |= dirFlag(dir);
|
||||
grid[n] |= dirFlag(dir * -1);
|
||||
|
||||
cells ~= n;
|
||||
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!added) {
|
||||
foreach(i; index .. cells.length - 1)
|
||||
cells[index] = cells[index + 1];
|
||||
cells = cells[0 .. $-1];
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
|
||||
/++
|
||||
Implements the A* path finding algorithm on a grid.
|
||||
|
||||
Params:
|
||||
start = starting point on the grid
|
||||
goal = destination point on the grid
|
||||
size = size of the grid
|
||||
isPassable = used to determine if the tile at the given coordinates are passible
|
||||
d = weight function to the A* algorithm. If null, assumes all will be equal weight. Returned value must be greater than or equal to 1.
|
||||
h = heuristic function to the A* algorithm. Gives an estimation of how many steps away the goal is from the given point to speed up the search. If null, assumes "taxicab distance"; the number of steps based solely on distance without diagonal movement. If you want to disable this entirely, pass `p => 0`.
|
||||
Returns:
|
||||
A list of waypoints to reach the destination, or `null` if impossible. The waypoints are returned in reverse order, starting from the goal and working back to the start.
|
||||
|
||||
So to get to the goal from the starting point, follow the returned array in $(B backwards).
|
||||
|
||||
The waypoints will not necessarily include every step but also may not only list turns, but if you follow
|
||||
them you will get get to the destination.
|
||||
|
||||
Bugs:
|
||||
The current implementation uses more memory than it really has to; it will eat like 8 MB of scratch space RAM on a 512x512 grid.
|
||||
|
||||
It doesn't consider wraparound possible so it might ask you to go all the way around the world unnecessarily.
|
||||
|
||||
History:
|
||||
Added May 2, 2020.
|
||||
+/
|
||||
Point[] pathfind(Point start, Point goal, Size size, scope bool delegate(Point) isPassable, scope int delegate(Point, Point) d = null, scope int delegate(Point) h = null) {
|
||||
|
||||
Point[] reconstruct_path(scope Point[] cameFrom, Point current) {
|
||||
Point[] totalPath;
|
||||
totalPath ~= current;
|
||||
|
||||
auto cf = cameFrom[current.y * size.width + current.x];
|
||||
while(cf != Point(int.min, int.min)) {
|
||||
current = cf;
|
||||
cf = cameFrom[current.y * size.width + current.x];
|
||||
totalPath ~= current;
|
||||
}
|
||||
return totalPath;
|
||||
}
|
||||
|
||||
// weighting thing.....
|
||||
static int d_default(Point a, Point b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(d is null)
|
||||
d = (Point a, Point b) => d_default(a, b);
|
||||
|
||||
if(h is null)
|
||||
h = (Point a) { return abs(a.y - goal.x) + abs(a.y - goal.y); };
|
||||
|
||||
Point[] openSet = [start];
|
||||
|
||||
Point[] cameFrom = new Point[](size.area);
|
||||
cameFrom[] = Point(int.min, int.min);
|
||||
|
||||
int[] gScore = new int[](size.area);
|
||||
gScore[] = int.max;
|
||||
gScore[start.y * size.width + start.x] = 0;
|
||||
|
||||
int[] fScore = new int[](size.area);
|
||||
fScore[] = int.max;
|
||||
fScore[start.y * size.width + start.x] = h(start);
|
||||
|
||||
while(openSet.length) {
|
||||
Point current;
|
||||
size_t currentIdx;
|
||||
int currentFscore = int.max;
|
||||
foreach(idx, pt; openSet) {
|
||||
auto p = fScore[pt.y * size.width + pt.x];
|
||||
if(p <= currentFscore) {
|
||||
currentFscore = p;
|
||||
current = pt;
|
||||
currentIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if(current == goal) {
|
||||
/+
|
||||
import std.stdio;
|
||||
foreach(y; 0 .. size.height)
|
||||
writefln("%(%02d,%)", gScore[y * size.width .. y * size.width + size.width]);
|
||||
+/
|
||||
return reconstruct_path(cameFrom, current);
|
||||
}
|
||||
|
||||
openSet[currentIdx] = openSet[$-1];
|
||||
openSet = openSet[0 .. $-1];
|
||||
|
||||
Point[4] neighborsBuffer;
|
||||
int neighborsBufferLength = 0;
|
||||
|
||||
if(current.x + 1 < size.width && isPassable(current + Point(1, 0)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(1, 0);
|
||||
if(current.x && isPassable(current + Point(-1, 0)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(-1, 0);
|
||||
if(current.y && isPassable(current + Point(0, -1)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(0, -1);
|
||||
if(current.y + 1 < size.height && isPassable(current + Point(0, 1)))
|
||||
neighborsBuffer[neighborsBufferLength++] = current + Point(0, 1);
|
||||
|
||||
foreach(neighbor; neighborsBuffer[0 .. neighborsBufferLength]) {
|
||||
auto tentative_gScore = gScore[current.y * size.width + current.x] + d(current, neighbor);
|
||||
if(tentative_gScore < gScore[neighbor.y * size.width + neighbor.x]) {
|
||||
cameFrom[neighbor.y * size.width + neighbor.x] = current;
|
||||
gScore[neighbor.y * size.width + neighbor.x] = tentative_gScore;
|
||||
fScore[neighbor.y * size.width + neighbor.x] = tentative_gScore + h(neighbor);
|
||||
// this linear thing might not be so smart after all
|
||||
bool found = false;
|
||||
foreach(o; openSet)
|
||||
if(o == neighbor) { found = true; break; }
|
||||
if(!found)
|
||||
openSet ~= neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue