parin/source/parin/engine.d
2024-12-08 20:53:28 +02:00

2320 lines
72 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ---
// Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT
// Email: alexandroskapretsos@gmail.com
// Project: https://github.com/Kapendev/parin
// Version: v0.0.28
// ---
// TODO: Test the resource loading code.
// TODO: Make sounds loop based on a variable and not on the file type.
// NOTE: The main problem with sound looping is the raylib API.
/// The `engine` module functions as a lightweight 2D game engine.
module parin.engine;
import rl = parin.rl;
import stdc = joka.stdc;
import joka.unions;
import joka.ascii;
import joka.io;
import parin.timer;
public import joka.colors;
public import joka.containers;
public import joka.faults;
public import joka.math;
public import joka.types;
@safe @nogc nothrow:
EngineState engineState;
IStr[64] engineEnvArgsBuffer;
Sz engineEnvArgsBufferLength;
/// A type representing flipping orientations.
enum Flip : ubyte {
none, /// No flipping.
x, /// Flipped along the X-axis.
y, /// Flipped along the Y-axis.
xy, /// Flipped along both X and Y axes.
}
/// A type representing texture filtering modes.
enum Filter : ubyte {
nearest = rl.TEXTURE_FILTER_POINT, /// Nearest neighbor filtering (blocky).
linear = rl.TEXTURE_FILTER_BILINEAR, /// Bilinear filtering (smooth).
}
/// A type representing texture wrapping modes.
enum Wrap : ubyte {
repeat = rl.TEXTURE_WRAP_REPEAT, // Repeats texture.
clamp = rl.TEXTURE_WRAP_CLAMP, // Clamps texture.
}
/// A type representing blending modes.
enum Blend : ubyte {
alpha = rl.BLEND_CUSTOM_SEPARATE, /// Standard alpha blending.
additive = rl.BLEND_ADDITIVE, /// Adds colors for light effects.
multiplied = rl.BLEND_MULTIPLIED, /// Multiplies colors for shadows.
add = rl.BLEND_ADD_COLORS, /// Simply adds colors.
sub = rl.BLEND_SUBTRACT_COLORS, /// Simply subtracts colors.
}
/// A type representing a limited set of keyboard keys.
enum Keyboard : ushort {
a = rl.KEY_A, /// The A key.
b = rl.KEY_B, /// The B key.
c = rl.KEY_C, /// The C key.
d = rl.KEY_D, /// The D key.
e = rl.KEY_E, /// The E key.
f = rl.KEY_F, /// The F key.
g = rl.KEY_G, /// The G key.
h = rl.KEY_H, /// The H key.
i = rl.KEY_I, /// The I key.
j = rl.KEY_J, /// The J key.
k = rl.KEY_K, /// The K key.
l = rl.KEY_L, /// The L key.
m = rl.KEY_M, /// The M key.
n = rl.KEY_N, /// The N key.
o = rl.KEY_O, /// The O key.
p = rl.KEY_P, /// The P key.
q = rl.KEY_Q, /// The Q key.
r = rl.KEY_R, /// The R key.
s = rl.KEY_S, /// The S key.
t = rl.KEY_T, /// The T key.
u = rl.KEY_U, /// The U key.
v = rl.KEY_V, /// The V key.
w = rl.KEY_W, /// The W key.
x = rl.KEY_X, /// The X key.
y = rl.KEY_Y, /// The Y key.
z = rl.KEY_Z, /// The Z key.
n0 = rl.KEY_ZERO, /// The 0 key.
n1 = rl.KEY_ONE, /// The 1 key.
n2 = rl.KEY_TWO, /// The 2 key.
n3 = rl.KEY_THREE, /// The 3 key.
n4 = rl.KEY_FOUR, /// The 4 key.
n5 = rl.KEY_FIVE, /// The 5 key.
n6 = rl.KEY_SIX, /// The 6 key.
n7 = rl.KEY_SEVEN, /// The 7 key.
n8 = rl.KEY_EIGHT, /// The 8 key.
n9 = rl.KEY_NINE, /// The 9 key.
nn0 = rl.KEY_KP_0, /// The 0 key on the numpad.
nn1 = rl.KEY_KP_1, /// The 1 key on the numpad.
nn2 = rl.KEY_KP_2, /// The 2 key on the numpad.
nn3 = rl.KEY_KP_3, /// The 3 key on the numpad.
nn4 = rl.KEY_KP_4, /// The 4 key on the numpad.
nn5 = rl.KEY_KP_5, /// The 5 key on the numpad.
nn6 = rl.KEY_KP_6, /// The 6 key on the numpad.
nn7 = rl.KEY_KP_7, /// The 7 key on the numpad.
nn8 = rl.KEY_KP_8, /// The 8 key on the numpad.
nn9 = rl.KEY_KP_9, /// The 9 key on the numpad.
f1 = rl.KEY_F1, /// The f1 key.
f2 = rl.KEY_F2, /// The f2 key.
f3 = rl.KEY_F3, /// The f3 key.
f4 = rl.KEY_F4, /// The f4 key.
f5 = rl.KEY_F5, /// The f5 key.
f6 = rl.KEY_F6, /// The f6 key.
f7 = rl.KEY_F7, /// The f7 key.
f8 = rl.KEY_F8, /// The f8 key.
f9 = rl.KEY_F9, /// The f9 key.
f10 = rl.KEY_F10, /// The f10 key.
f11 = rl.KEY_F11, /// The f11 key.
f12 = rl.KEY_F12, /// The f12 key.
left = rl.KEY_LEFT, /// The left arrow key.
right = rl.KEY_RIGHT, /// The right arrow key.
up = rl.KEY_UP, /// The up arrow key.
down = rl.KEY_DOWN, /// The down arrow key.
esc = rl.KEY_ESCAPE, /// The escape key.
enter = rl.KEY_ENTER, /// The enter key.
tab = rl.KEY_TAB, /// The tab key.
space = rl.KEY_SPACE, /// The space key.
backspace = rl.KEY_BACKSPACE, /// THe backspace key.
shift = rl.KEY_LEFT_SHIFT, /// The left shift key.
ctrl = rl.KEY_LEFT_CONTROL, /// The left control key.
alt = rl.KEY_LEFT_ALT, /// The left alt key.
win = rl.KEY_LEFT_SUPER, /// The left windows/super/command key.
insert = rl.KEY_INSERT, /// The insert key.
del = rl.KEY_DELETE, /// The delete key.
home = rl.KEY_HOME, /// The home key.
end = rl.KEY_END, /// The end key.
pageUp = rl.KEY_PAGE_UP, /// The page up key.
pageDown = rl.KEY_PAGE_DOWN, /// The page down key.
}
/// A type representing a limited set of mouse keys.
enum Mouse : ubyte {
left = rl.MOUSE_BUTTON_LEFT, /// The left mouse button.
right = rl.MOUSE_BUTTON_RIGHT, /// The right mouse button.
middle = rl.MOUSE_BUTTON_MIDDLE, /// The middle mouse button.
}
/// A type representing a limited set of gamepad buttons.
enum Gamepad : ubyte {
left = rl.GAMEPAD_BUTTON_LEFT_FACE_LEFT, /// The left button.
right = rl.GAMEPAD_BUTTON_LEFT_FACE_RIGHT, /// The right button.
up = rl.GAMEPAD_BUTTON_LEFT_FACE_UP, /// The up button.
down = rl.GAMEPAD_BUTTON_LEFT_FACE_DOWN, /// The down button.
y = rl.GAMEPAD_BUTTON_RIGHT_FACE_UP, /// The Xbox y, PlayStation triangle and Nintendo x button.
x = rl.GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, /// The Xbox x, PlayStation square and Nintendo y button.
a = rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN, /// The Xbox a, PlayStation cross and Nintendo b button.
b = rl.GAMEPAD_BUTTON_RIGHT_FACE_LEFT, /// The Xbox b, PlayStation circle and Nintendo a button.
lt = rl.GAMEPAD_BUTTON_LEFT_TRIGGER_2, /// The left trigger button.
lb = rl.GAMEPAD_BUTTON_LEFT_TRIGGER_1, /// The left bumper button.
lsb = rl.GAMEPAD_BUTTON_LEFT_THUMB, /// The left stick button.
rt = rl.GAMEPAD_BUTTON_RIGHT_TRIGGER_2, /// The right trigger button.
rb = rl.GAMEPAD_BUTTON_RIGHT_TRIGGER_1, /// The right bumper button.
rsb = rl.GAMEPAD_BUTTON_RIGHT_THUMB, /// The right stick button.
back = rl.GAMEPAD_BUTTON_MIDDLE_LEFT, /// The back button.
start = rl.GAMEPAD_BUTTON_MIDDLE_RIGHT, /// The start button.
middle = rl.GAMEPAD_BUTTON_MIDDLE, /// The middle button.
}
/// A structure containing options for configuring drawing parameters.
struct DrawOptions {
Vec2 origin = Vec2(0.0f); /// The origin point of the drawn object.
Vec2 scale = Vec2(1.0f); /// The scale of the drawn object.
float rotation = 0.0f; /// The rotation of the drawn object, in degrees.
Color color = white; /// The color of the drawn object.
Hook hook = Hook.topLeft; /// A value representing the origin point of the drawn object when origin is set to zero.
Flip flip = Flip.none; /// A value representing flipping orientations.
@safe @nogc nothrow:
/// Initializes the options with the given rotation.
this(float rotation) {
this.rotation = rotation;
}
/// Initializes the options with the given scale.
this(Vec2 scale) {
this.scale = scale;
}
/// Initializes the options with the given color.
this(Color color) {
this.color = color;
}
/// Initializes the options with the given hook.
this(Hook hook) {
this.hook = hook;
}
/// Initializes the options with the given flip.
this(Flip flip) {
this.flip = flip;
}
}
/// Represents an identifier for a managed resource.
struct TextId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
/// Returns the length of the text associated with the resource identifier.
Sz length() {
return getOr().length;
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value != 0 && engineState.resources.texts.has(data);
}
/// Retrieves the text associated with the resource identifier.
ref LStr get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.texts.data[data];
}
/// Retrieves the text associated with the resource identifier or returns a default value if invalid.
LStr getOr() {
return isValid ? get() : LStr();
}
/// Frees the resource associated with the identifier.
void free() {
if (engineState.resources.texts.has(data)) {
engineState.resources.texts.remove(data);
}
}
}
/// Represents a texture resource.
struct Texture {
rl.Texture2D data;
@safe @nogc nothrow:
/// Checks if the texture is not loaded.
bool isEmpty() {
return data.id <= 0;
}
/// Returns the width of the texture.
int width() {
return data.width;
}
/// Returns the height of the texture.
int height() {
return data.height;
}
/// Returns the size of the texture.
Vec2 size() {
return Vec2(width, height);
}
/// Sets the filter mode of the texture.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data, value);
}
/// Sets the wrap mode of the texture.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data, value);
}
/// Frees the loaded texture.
@trusted
void free() {
if (isEmpty) {
return;
}
rl.UnloadTexture(data);
this = Texture();
}
}
/// Represents an identifier for a managed resource.
struct TextureId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
/// Returns the width of the texture associated with the resource identifier.
int width() {
return getOr().width;
}
/// Returns the height of the texture associated with the resource identifier.
int height() {
return getOr().height;
}
/// Returns the size of the texture associated with the resource identifier.
Vec2 size() {
return getOr().size;
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value != 0 && engineState.resources.textures.has(data);
}
/// Retrieves the texture associated with the resource identifier.
ref Texture get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.textures.data[data];
}
/// Retrieves the texture associated with the resource identifier or returns a default value if invalid.
Texture getOr() {
return isValid ? get() : Texture();
}
/// Frees the resource associated with the identifier.
void free() {
if (engineState.resources.textures.has(data)) {
engineState.resources.textures.remove(data);
}
}
}
/// Represents a font resource.
struct Font {
rl.Font data;
int runeSpacing; /// The spacing between individual characters.
int lineSpacing; /// The spacing between lines of text.
@safe @nogc nothrow:
/// Checks if the font is not loaded.
bool isEmpty() {
return data.texture.id <= 0;
}
/// Returns the size of the font.
int size() {
return data.baseSize;
}
/// Sets the filter mode of the font.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data.texture, value);
}
/// Sets the wrap mode of the font.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data.texture, value);
}
/// Frees the loaded font.
@trusted
void free() {
if (isEmpty) {
return;
}
rl.UnloadFont(data);
this = Font();
}
}
/// Represents an identifier for a managed resource.
struct FontId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
/// Returns the spacing between individual characters of the font associated with the resource identifier.
int runeSpacing() {
return getOr().runeSpacing;
}
/// Returns the spacing between lines of text of the font associated with the resource identifier.
int lineSpacing() {
return getOr().lineSpacing;
};
/// Returns the size of the font associated with the resource identifier.
int size() {
return getOr().size;
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value != 0 && engineState.resources.fonts.has(data);
}
/// Retrieves the font associated with the resource identifier.
ref Font get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.fonts.data[data];
}
/// Retrieves the font associated with the resource identifier or returns a default value if invalid.
Font getOr() {
return isValid ? get() : Font();
}
/// Frees the resource associated with the identifier.
void free() {
if (engineState.resources.fonts.has(data)) {
engineState.resources.fonts.remove(data);
}
}
}
/// Represents a sound resource.
struct Sound {
Variant!(rl.Sound, rl.Music) data;
@safe @nogc nothrow:
/// Checks if the sound is not loaded.
bool isEmpty() {
if (data.isKind!(rl.Sound)) {
return data.get!(rl.Sound)().stream.sampleRate == 0;
} else {
return data.get!(rl.Music)().stream.sampleRate == 0;
}
}
/// Returns true if the sound is playing.
@trusted
bool isPlaying() {
if (data.isKind!(rl.Sound)) {
return rl.IsSoundPlaying(data.get!(rl.Sound)());
} else {
return rl.IsMusicStreamPlaying(data.get!(rl.Music)());
}
}
/// Returns the current playback time of the sound.
@trusted
float time() {
if (data.isKind!(rl.Sound)) {
return 0.0f;
} else {
return rl.GetMusicTimePlayed(data.get!(rl.Music)());
}
}
/// Returns the total duration of the sound.
@trusted
float duration() {
if (data.isKind!(rl.Sound)) {
return 0.0f;
} else {
return rl.GetMusicTimeLength(data.get!(rl.Music)());
}
}
/// Returns the progress of the sound.
float progress() {
if (duration == 0.0f) return 0.0f;
return time / duration;
}
/// Sets the volume level for the sound.
@trusted
void setVolume(float value) {
if (data.isKind!(rl.Sound)) {
rl.SetSoundVolume(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicVolume(data.get!(rl.Music)(), value);
}
}
/// Sets the pitch of the sound.
@trusted
void setPitch(float value) {
if (data.isKind!(rl.Sound)) {
rl.SetSoundPitch(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPitch(data.get!(rl.Music)(), value);
}
}
/// Sets the stereo panning of the sound.
@trusted
void setPan(float value) {
if (data.isKind!(rl.Sound)) {
rl.SetSoundPan(data.get!(rl.Sound)(), value);
} else {
rl.SetMusicPan(data.get!(rl.Music)(), value);
}
}
/// Frees the loaded sound.
@trusted
void free() {
if (isEmpty) return;
if (data.isKind!(rl.Sound)) {
rl.UnloadSound(data.get!(rl.Sound)());
} else {
rl.UnloadMusicStream(data.get!(rl.Music)());
}
this = Sound();
}
}
/// Represents an identifier for a managed resource.
struct SoundId {
GenerationalIndex data;
alias data this;
@safe @nogc nothrow:
/// Returns the current playback time of the sound associated with the resource identifier.
float time() {
return getOr().time;
}
/// Returns the total duration of the sound associated with the resource identifier.
float duration() {
return getOr().duration;
}
float progress() {
return getOr().progress;
}
/// Checks if the resource identifier is valid. It becomes automatically invalid when the resource is freed.
bool isValid() {
return data.value != 0 && engineState.resources.sounds.has(data);
}
/// Retrieves the sound associated with the resource identifier.
ref Sound get() {
if (!isValid) {
assert(0, "Index `{}` with generation `{}` does not exist.".format(data.value, data.generation));
}
return engineState.resources.sounds.data[data];
}
/// Retrieves the sound associated with the resource identifier or returns a default value if invalid.
Sound getOr() {
return isValid ? get() : Sound();
}
/// Frees the resource associated with the identifier.
void free() {
if (engineState.resources.sounds.has(data)) {
engineState.resources.sounds.remove(data);
}
}
}
/// Represents the viewing area for rendering.
struct Viewport {
rl.RenderTexture2D data;
Color color; /// The background color of the viewport.
Blend blend; /// A value representing blending modes.
bool isAttached; /// Indicates whether the viewport is currently in use.
@safe @nogc nothrow:
/// Initializes the viewport with the given size, background color and blend mode.
this(Color color, Blend blend = Blend.alpha) {
this.color = color;
this.blend = blend;
}
/// Checks if the viewport is not loaded.
bool isEmpty() {
return data.texture.id <= 0;
}
/// Returns the width of the viewport.
int width() {
return data.texture.width;
}
/// Returns the height of the viewport.
int height() {
return data.texture.height;
}
/// Returns the size of the viewport.
Vec2 size() {
return Vec2(width, height);
}
/// Resizes the viewport to the given width and height.
/// Internally, this allocates a new render texture, so avoid calling it while the viewport is in use.
@trusted
void resize(int width, int height) {
if (!isEmpty) rl.UnloadRenderTexture(data);
data = rl.LoadRenderTexture(width, height);
setFilter(engineState.defaultFilter);
setWrap(engineState.defaultWrap);
}
/// Attaches the viewport, making it active.
// NOTE: The engine viewport should not use this function.
@trusted
void attach() {
if (isEmpty) return;
if (engineState.currentViewport.isAttached) {
assert(0, "Cannot attach viewport because another viewport is already attached.");
}
isAttached = true;
engineState.currentViewport = this;
if (isResolutionLocked) rl.EndTextureMode();
rl.BeginTextureMode(data);
rl.ClearBackground(color.toRl());
rl.BeginBlendMode(blend);
}
/// Detaches the viewport, making it inactive.
// NOTE: The engine viewport should not use this function.
@trusted
void detach() {
if (isEmpty) return;
if (!isAttached) {
assert(0, "Cannot detach viewport because it is not the attached viewport.");
}
isAttached = false;
engineState.currentViewport = Viewport();
rl.EndBlendMode();
rl.EndTextureMode();
if (isResolutionLocked) rl.BeginTextureMode(engineState.viewport.toRl());
}
/// Sets the filter mode of the viewport.
@trusted
void setFilter(Filter value) {
if (isEmpty) return;
rl.SetTextureFilter(data.texture, value);
}
/// Sets the wrap mode of the viewport.
@trusted
void setWrap(Wrap value) {
if (isEmpty) return;
rl.SetTextureWrap(data.texture, value);
}
/// Frees the loaded viewport.
@trusted
void free() {
if (isEmpty) return;
rl.UnloadRenderTexture(data);
this = Viewport();
}
}
/// A structure representing a camera.
struct Camera {
Vec2 position; /// The position of the cammera.
float rotation = 0.0f; /// The rotation angle of the camera, in degrees.
float scale = 1.0f; /// The zoom level of the camera.
bool isCentered; /// Determines if the camera's origin is at the center instead of the top left.
bool isAttached; /// Indicates whether the camera is currently in use.
@safe @nogc nothrow:
/// Initializes the camera with the given position and optional centering.
this(float x, float y, bool isCentered = false) {
this.position.x = x;
this.position.y = y;
this.isCentered = isCentered;
}
/// Returns the current hook associated with the camera.
Hook hook() {
return isCentered ? Hook.center : Hook.topLeft;
}
/// Returns the origin of the camera.
Vec2 origin(Viewport viewport = Viewport()) {
if (viewport.isEmpty) {
return Rect(resolution / Vec2(scale)).origin(hook);
} else {
return Rect(viewport.size / Vec2(scale)).origin(hook);
}
}
/// Returns the area covered by the camera.
Rect area(Viewport viewport = Viewport()) {
if (viewport.isEmpty) {
return Rect(position, resolution / Vec2(scale)).area(hook);
} else {
return Rect(position, viewport.size / Vec2(scale)).area(hook);
}
}
/// Returns the top left point of the camera.
Vec2 topLeftPoint() {
return area.topLeftPoint;
}
/// Returns the top point of the camera.
Vec2 topPoint() {
return area.topPoint;
}
/// Returns the top right point of the camera.
Vec2 topRightPoint() {
return area.topRightPoint;
}
/// Returns the left point of the camera.
Vec2 leftPoint() {
return area.leftPoint;
}
/// Returns the center point of the camera.
Vec2 centerPoint() {
return area.centerPoint;
}
/// Returns the right point of the camera.
Vec2 rightPoint() {
return area.rightPoint;
}
/// Returns the bottom left point of the camera.
Vec2 bottomLeftPoint() {
return area.bottomLeftPoint;
}
/// Returns the bottom point of the camera.
Vec2 bottomPoint() {
return area.bottomPoint;
}
/// Returns the bottom right point of the camera.
Vec2 bottomRightPoint() {
return area.bottomRightPoint;
}
/// Moves the camera to follow the target position at the specified speed.
void followPosition(Vec2 target, float speed) {
position = position.moveTo(target, Vec2(speed));
}
/// Moves the camera to follow the target position with gradual slowdown.
void followPositionWithSlowdown(Vec2 target, float slowdown) {
position = position.moveToWithSlowdown(target, Vec2(deltaTime), slowdown);
}
/// Adjusts the cameras zoom level to follow the target value at the specified speed.
void followScale(float target, float speed) {
scale = scale.moveTo(target, speed);
}
/// Adjusts the cameras zoom level to follow the target value with gradual slowdown.
void followScaleWithSlowdown(float target, float slowdown) {
scale = scale.moveToWithSlowdown(target, deltaTime, slowdown);
}
/// Attaches the camera, making it active.
@trusted
void attach() {
if (engineState.currentCamera.isAttached) {
assert(0, "Cannot attach camera because another camera is already attached.");
}
isAttached = true;
engineState.currentCamera = this;
auto temp = this.toRl(engineState.currentViewport);
if (isPixelSnapped || isPixelPerfect) {
temp.target.x = floor(temp.target.x);
temp.target.y = floor(temp.target.y);
temp.offset.x = floor(temp.offset.x);
temp.offset.y = floor(temp.offset.y);
}
rl.BeginMode2D(temp);
}
/// Detaches the camera, making it inactive.
@trusted
void detach() {
if (!isAttached) {
assert(0, "Cannot detach camera because it is not the attached camera.");
}
isAttached = false;
engineState.currentCamera = Camera();
rl.EndMode2D();
}
}
struct EngineFlags {
bool isUpdating;
bool isPixelSnapped;
bool isPixelPerfect;
bool isCursorVisible;
bool canUseAssetsPath;
}
struct EngineFullscreenState {
int lastWindowWidth;
int lastWindowHeight;
Timer toggleTimer = Timer(0.1f);
}
struct EngineResourceGroup(T) {
GenerationalList!T data;
GenerationalList!LStr names;
GenerationalList!Sz tags;
@safe @nogc nothrow:
Sz length() {
return data.length;
}
bool has(GenerationalIndex i) {
return data.has(i);
}
GenerationalIndex append(T arg, IStr name, Sz tag) {
data.append(arg);
names.append(LStr(name));
return tags.append(tag);
}
GenerationalIndex appendEmpty() {
return append(T(), "", 0);
}
void remove(GenerationalIndex i) {
data[i].free();
data.remove(i);
names[i].free();
names.remove(i);
tags.remove(i);
}
void free(Sz tag = 0) {
if (tag == 0) {
foreach (ref item; data.items) {
item.free();
}
data.free();
foreach (ref item; names.items) {
item.free();
}
names.free();
tags.free();
} else {
foreach (id; data.ids) {
if (tag == tags[id]) {
remove(id);
}
}
}
}
auto items() {
return data.items;
}
auto ids() {
return data.ids;
}
}
struct EngineResources {
EngineResourceGroup!LStr texts;
EngineResourceGroup!Texture textures;
EngineResourceGroup!Font fonts;
EngineResourceGroup!Sound sounds;
@safe @nogc nothrow:
void free(Sz tag = 0) {
texts.free(tag);
textures.free(tag);
fonts.free(tag);
sounds.free(tag);
}
}
// NOTE: Maybe look at the locking and unlocking code again. Works, but maybe could be more nice looking.
struct EngineViewport {
Viewport data;
int targetWidth;
int targetHeight;
alias data this;
@safe @nogc nothrow:
bool isLocking() {
return (targetWidth != 0 && targetHeight != 0) && (data.width != targetWidth && data.height != targetHeight);
}
bool isUnlocking() {
return (targetWidth == 0 && targetHeight == 0) && (!data.isEmpty);
}
void startLocking(int width, int height) {
targetWidth = width;
targetHeight = height;
}
void startUnlocking() {
targetWidth = 0;
targetHeight = 0;
}
}
struct EngineState {
EngineFlags flags;
EngineViewport viewport;
EngineResources resources;
EngineFullscreenState fullscreenState;
Color borderColor;
Sz tickCount;
LStr assetsPath;
LStr tempText;
Camera currentCamera;
Viewport currentViewport;
Filter defaultFilter;
Wrap defaultWrap;
rl.FilePathList filePathListBuffer;
@safe @nogc nothrow:
void free() {
debug {
println("Resources that will be freed automatically:");
println(" Text count: ", resources.texts.length != 0 ? resources.texts.length - 1 : 0);
println(" Texture count: ", resources.textures.length != 0 ? resources.textures.length - 1 : 0);
println(" Font count: ", resources.fonts.length != 0 ? resources.fonts.length - 1 : 0);
println(" Sound count: ", resources.sounds.length != 0 ? resources.sounds.length - 1 : 0);
}
viewport.free();
resources.free();
tempText.free();
assetsPath.free();
this = EngineState();
}
}
/// Converts a raylib type to a Parin type.
Color toParin(rl.Color from) {
return Color(from.r, from.g, from.b, from.a);
}
/// Converts a raylib type to a Parin type.
Vec2 toParin(rl.Vector2 from) {
return Vec2(from.x, from.y);
}
/// Converts a raylib type to a Parin type.
Vec3 toParin(rl.Vector3 from) {
return Vec3(from.x, from.y, from.z);
}
/// Converts a raylib type to a Parin type.
Vec4 toParin(rl.Vector4 from) {
return Vec4(from.x, from.y, from.z, from.w);
}
/// Converts a raylib type to a Parin type.
Rect toParin(rl.Rectangle from) {
return Rect(from.x, from.y, from.width, from.height);
}
/// Converts a raylib type to a Parin type.
Texture toParin(rl.Texture2D from) {
auto result = Texture();
result.data = from;
return result;
}
/// Converts a raylib type to a Parin type.
Font toParin(rl.Font from) {
auto result = Font();
result.data = from;
return result;
}
/// Converts a raylib type to a Parin type.
Viewport toParin(rl.RenderTexture2D from) {
auto result = Viewport();
result.data = from;
return result;
}
/// Converts a Parin type to a raylib type.
rl.Color toRl(Color from) {
return rl.Color(from.r, from.g, from.b, from.a);
}
/// Converts a Parin type to a raylib type.
rl.Vector2 toRl(Vec2 from) {
return rl.Vector2(from.x, from.y);
}
/// Converts a Parin type to a raylib type.
rl.Vector3 toRl(Vec3 from) {
return rl.Vector3(from.x, from.y, from.z);
}
/// Converts a Parin type to a raylib type.
rl.Vector4 toRl(Vec4 from) {
return rl.Vector4(from.x, from.y, from.z, from.w);
}
/// Converts a Parin type to a raylib type.
rl.Rectangle toRl(Rect from) {
return rl.Rectangle(from.position.x, from.position.y, from.size.x, from.size.y);
}
/// Converts a Parin type to a raylib type.
rl.Texture2D toRl(Texture from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
rl.Font toRl(Font from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
rl.RenderTexture2D toRl(Viewport from) {
return from.data;
}
/// Converts a Parin type to a raylib type.
int toRl(Filter filter) {
return filter;
}
/// Converts a Parin type to a raylib type.
rl.Camera2D toRl(Camera camera, Viewport viewport = Viewport()) {
auto area = Rect(viewport.isEmpty ? resolution : viewport.size);
return rl.Camera2D(
area.origin(camera.isCentered ? Hook.center : Hook.topLeft).toRl(),
camera.position.toRl(),
camera.rotation,
camera.scale,
);
}
/// Converts an ASCII bitmap font texture into a font.
/// The texture will be freed when the font is freed.
@trusted
Font toAsciiFont(Texture texture, int tileWidth, int tileHeight) {
if (texture.isEmpty || tileWidth <= 0|| tileHeight <= 0) return Font();
auto result = Font();
result.lineSpacing = tileHeight;
auto rowCount = texture.height / tileHeight;
auto colCount = texture.width / tileWidth;
auto maxCount = rowCount * colCount;
result.data.baseSize = tileHeight;
result.data.glyphCount = maxCount;
result.data.glyphPadding = 0;
result.data.texture = texture.data;
result.data.recs = cast(rl.Rectangle*) stdc.malloc(maxCount * rl.Rectangle.sizeof);
foreach (i; 0 .. maxCount) {
result.data.recs[i].x = (i % colCount) * tileWidth;
result.data.recs[i].y = (i / colCount) * tileHeight;
result.data.recs[i].width = tileWidth;
result.data.recs[i].height = tileHeight;
}
result.data.glyphs = cast(rl.GlyphInfo*) stdc.malloc(maxCount * rl.GlyphInfo.sizeof);
foreach (i; 0 .. maxCount) {
result.data.glyphs[i] = rl.GlyphInfo();
result.data.glyphs[i].value = i + 32;
}
return result;
}
/// Returns the opposite flip value.
/// The opposite of every flip value except none is none.
/// The fallback value is returned if the flip value is none.
Flip oppositeFlip(Flip flip, Flip fallback) {
return flip == fallback ? Flip.none : fallback;
}
/// Returns the arguments that this application was started with.
IStr[] envArgs() {
return engineEnvArgsBuffer[0 .. engineEnvArgsBufferLength];
}
/// Returns a random integer between 0 and int.max (inclusive).
@trusted
int randi() {
return rl.GetRandomValue(0, int.max);
}
/// Returns a random floating point number between 0.0 and 1.0 (inclusive).
@trusted
float randf() {
return rl.GetRandomValue(0, cast(int) float.max) / cast(float) cast(int) float.max;
}
/// Sets the seed of the random number generator to the given value.
@trusted
void randomize(int seed) {
rl.SetRandomSeed(seed);
}
/// Randomizes the seed of the random number generator.
void randomize() {
randomize(randi);
}
/// Converts a world point to a screen point based on the given camera.
@trusted
Vec2 toScreenPoint(Vec2 position, Camera camera, Viewport viewport = Viewport()) {
return toParin(rl.GetWorldToScreen2D(position.toRl(), camera.toRl(viewport)));
}
/// Converts a screen point to a world point based on the given camera.
@trusted
Vec2 toWorldPoint(Vec2 position, Camera camera, Viewport viewport = Viewport()) {
return toParin(rl.GetScreenToWorld2D(position.toRl(), camera.toRl(viewport)));
}
/// Returns an absolute path to the assets folder.
IStr assetsPath() {
return engineState.assetsPath.items;
}
/// Converts a relative path to an absolute path within the assets folder.
IStr toAssetsPath(IStr path) {
return pathConcat(assetsPath, path).pathFormat();
}
/// Returns true if the assets path is currently in use when loading.
bool canUseAssetsPath() {
return engineState.flags.canUseAssetsPath;
}
/// Sets whether the assets path should be in use when loading.
void setCanUseAssetsPath(bool value) {
engineState.flags.canUseAssetsPath = value;
}
/// Returns the dropped file paths of the current frame.
@trusted
IStr[] droppedFilePaths() {
static IStr[128] buffer;
foreach (i; 0 .. engineState.filePathListBuffer.count) {
buffer[i] = engineState.filePathListBuffer.paths[i].toStr();
}
return buffer[0 .. engineState.filePathListBuffer.count];
}
/// Loads a text file from the assets folder.
/// The resource remains valid until this function is called again.
/// Supports both forward slashes and backslashes in file paths.
Result!IStr loadTempText(IStr path) {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
auto fault = readTextIntoBuffer(targetPath, engineState.tempText);
return Result!IStr(engineState.tempText.items, fault);
}
/// Loads a text file from the assets folder.
/// The resource must be manually freed.
/// Supports both forward slashes and backslashes in file paths.
Result!LStr loadRawText(IStr path) {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
return readText(targetPath);
}
/// Loads a text file from the assets folder.
/// Optionally assigns a tag for resource management.
/// The resource is managed by the engine and can be freed manually or with the `freeResources` function.
/// Supports both forward slashes and backslashes in file paths.
TextId loadText(IStr path, Sz tag = 0) {
if (engineState.resources.texts.length == 0) {
engineState.resources.texts.appendEmpty();
}
foreach (id; engineState.resources.texts.ids) {
if (engineState.resources.texts.names[id] == path) {
return TextId(id);
}
}
auto result = loadRawText(path);
if (result.isSome) {
return TextId(engineState.resources.texts.append(result.get(), path, tag));
} else {
return TextId();
}
}
/// Loads a texture file (PNG) from the assets folder.
/// The resource must be manually freed.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Texture loadRawTexture(IStr path) {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
auto value = rl.LoadTexture(targetPath.toCStr().getOr()).toParin();
value.setFilter(engineState.defaultFilter);
value.setWrap(engineState.defaultWrap);
return Result!Texture(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a texture file (PNG) from the assets folder.
/// Optionally assigns a tag for resource management.
/// The resource is managed by the engine and can be freed manually or with the `freeResources` function.
/// Supports both forward slashes and backslashes in file paths.
TextureId loadTexture(IStr path, Sz tag = 0) {
if (engineState.resources.textures.length == 0) {
engineState.resources.textures.appendEmpty();
}
foreach (id; engineState.resources.textures.ids) {
if (engineState.resources.textures.names[id] == path) {
return TextureId(id);
}
}
auto result = loadRawTexture(path);
if (result.isSome) {
return TextureId(engineState.resources.textures.append(result.get(), path, tag));
} else {
return TextureId();
}
}
/// Loads a font file (TTF) from the assets folder.
/// The resource must be manually freed.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Font loadRawFont(IStr path, int size, int runeSpacing, int lineSpacing, IStr32 runes = "") {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
auto value = rl.LoadFontEx(targetPath.toCStr().getOr(), size, runes == "" ? null : cast(int*) runes.ptr, cast(int) runes.length).toParin();
if (value.data.texture.id == engineFont.data.texture.id) {
value = Font();
}
value.runeSpacing = runeSpacing;
value.lineSpacing = lineSpacing;
value.setFilter(engineState.defaultFilter);
value.setWrap(engineState.defaultWrap);
return Result!Font(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a font file (TTF) from the assets folder.
/// Optionally assigns a tag for resource management.
/// The resource is managed by the engine and can be freed manually or with the `freeResources` function.
/// Supports both forward slashes and backslashes in file paths.
FontId loadFont(IStr path, int size, int runeSpacing, int lineSpacing, IStr32 runes = "", Sz tag = 0) {
if (engineState.resources.fonts.length == 0) {
engineState.resources.fonts.appendEmpty();
}
foreach (id; engineState.resources.fonts.ids) {
if (engineState.resources.fonts.names[id] == path) {
return FontId(id);
}
}
auto result = loadRawFont(path, size, runeSpacing, lineSpacing, runes);
if (result.isSome) {
return FontId(FontId(engineState.resources.fonts.append(result.get(), path, tag)));
} else {
return FontId();
}
}
/// Loads an ASCII bitmap font file (PNG) from the assets folder.
/// The resource must be manually freed.
/// Supports both forward slashes and backslashes in file paths.
Result!Font loadRawAsciiFont(IStr path, int tileWidth, int tileHeight) {
auto value = loadRawTexture(path).getOr();
return Result!Font(value.toAsciiFont(tileWidth, tileHeight), value.isEmpty.toFault(Fault.cantFind));
}
/// Loads an ASCII bitmap font file (PNG) from the assets folder.
/// Optionally assigns a tag for resource management.
/// The resource is managed by the engine and can be freed manually or with the `freeResources` function.
/// Supports both forward slashes and backslashes in file paths.
FontId loadAsciiFont(IStr path, int tileWidth, int tileHeight, Sz tag = 0) {
if (engineState.resources.fonts.length == 0) {
engineState.resources.fonts.appendEmpty();
}
foreach (id; engineState.resources.fonts.ids) {
if (engineState.resources.fonts.names[id] == path) {
return FontId(id);
}
}
auto result = loadRawAsciiFont(path, tileWidth, tileHeight);
if (result.isSome) {
return FontId(FontId(engineState.resources.fonts.append(result.get(), path, tag)));
} else {
return FontId();
}
}
/// Loads a sound file (WAV, OGG, MP3) from the assets folder.
/// The resource must be manually freed.
/// Supports both forward slashes and backslashes in file paths.
@trusted
Result!Sound loadRawSound(IStr path, float volume, float pitch) {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
auto value = Sound();
if (path.endsWith(".wav")) {
value.data = rl.LoadSound(targetPath.toCStr().getOr());
} else {
value.data = rl.LoadMusicStream(targetPath.toCStr().getOr());
}
value.setVolume(volume);
value.setPitch(pitch);
return Result!Sound(value, value.isEmpty.toFault(Fault.cantFind));
}
/// Loads a sound file (WAV, OGG, MP3) from the assets folder.
/// Optionally assigns a tag for resource management.
/// The resource is managed by the engine and can be freed manually or with the `freeResources` function.
/// Supports both forward slashes and backslashes in file paths.
SoundId loadSound(IStr path, float volume, float pitch, Sz tag = 0) {
if (engineState.resources.sounds.length == 0) {
engineState.resources.sounds.appendEmpty();
}
foreach (id; engineState.resources.sounds.ids) {
if (engineState.resources.sounds.names[id] == path) {
return SoundId(id);
}
}
auto result = loadRawSound(path, volume, pitch);
if (result.isSome) {
return SoundId(engineState.resources.sounds.append(result.get(), path, tag));
} else {
return SoundId();
}
}
/// Saves a text file to the assets folder.
/// Supports both forward slashes and backslashes in file paths.
Fault saveText(IStr path, IStr text) {
auto targetPath = canUseAssetsPath ? path.toAssetsPath() : path;
return writeText(targetPath, text);
}
/// Frees all managed resources associated with the given tag, or all if no tag is specified.
void freeResources(Sz tag = 0) {
engineState.resources.free(tag);
}
/// Opens a window with the specified size and title.
/// You should avoid calling this function manually.
@trusted
void openWindow(int width, int height, IStr appPath, IStr title = "Parin") {
if (rl.IsWindowReady) {
return;
}
rl.SetConfigFlags(rl.FLAG_WINDOW_RESIZABLE | rl.FLAG_VSYNC_HINT);
rl.SetTraceLogLevel(rl.LOG_ERROR);
rl.InitWindow(width, height, title.toCStr().getOr());
rl.InitAudioDevice();
rl.SetExitKey(rl.KEY_NULL);
rl.SetTargetFPS(60);
engineState.borderColor = black;
engineState.viewport.color = gray;
engineState.fullscreenState.lastWindowWidth = width;
engineState.fullscreenState.lastWindowHeight = height;
engineState.flags.canUseAssetsPath = true;
engineState.assetsPath.append(pathConcat(appPath.pathDir, "assets"));
engineState.tempText.reserve(8192);
// NOTE: This line is used for fixing an alpha bug with render textures.
rl.rlSetBlendFactorsSeparate(0x0302, 0x0303, 1, 0x0303, 0x8006, 0x8006);
}
/// Updates the window every frame with the given function.
/// This function will return when the given function returns true.
/// You should avoid calling this function manually.
@trusted
void updateWindow(bool function(float dt) updateFunc) {
static bool function(float _dt) @trusted @nogc nothrow _updateFunc;
@trusted @nogc nothrow
static bool _updateWindow() {
// Begin drawing.
if (isResolutionLocked) {
rl.BeginTextureMode(engineState.viewport.toRl());
} else {
rl.BeginDrawing();
}
rl.ClearBackground(engineState.viewport.color.toRl());
// The main loop.
if (rl.IsFileDropped) {
engineState.filePathListBuffer = rl.LoadDroppedFiles();
}
auto dt = deltaTime;
auto result = _updateFunc(dt);
engineState.tickCount = (engineState.tickCount + 1) % engineState.tickCount.max;
if (rl.IsFileDropped) {
rl.UnloadDroppedFiles(engineState.filePathListBuffer);
engineState.filePathListBuffer = rl.FilePathList();
}
// End drawing.
if (isResolutionLocked) {
auto minSize = engineState.viewport.size;
auto maxSize = windowSize;
auto ratio = maxSize / minSize;
auto minRatio = min(ratio.x, ratio.y);
if (isPixelPerfect) {
auto roundMinRatio = round(minRatio);
auto floorMinRation = floor(minRatio);
minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation;
}
auto targetSize = minSize * Vec2(minRatio);
auto targetPosition = maxSize * Vec2(0.5f) - targetSize * Vec2(0.5f);
rl.EndTextureMode();
rl.BeginDrawing();
rl.ClearBackground(engineState.borderColor.toRl());
rl.DrawTexturePro(
engineState.viewport.toRl().texture,
rl.Rectangle(0.0f, 0.0f, minSize.x, -minSize.y),
rl.Rectangle(
floor(targetPosition.x),
floor(targetPosition.y),
ratio.x == minRatio ? targetSize.x : floor(targetSize.x),
ratio.y == minRatio ? targetSize.y : floor(targetSize.y),
),
rl.Vector2(0.0f, 0.0f),
0.0f,
rl.Color(255, 255, 255, 255),
);
rl.EndDrawing();
} else {
rl.EndDrawing();
}
// Main viewport code.
if (engineState.viewport.isLocking) {
engineState.viewport.resize(engineState.viewport.targetWidth, engineState.viewport.targetHeight);
} else if (engineState.viewport.isUnlocking) {
engineState.viewport.free();
}
// Fullscreen code to fix a bug on Linux.
engineState.fullscreenState.toggleTimer.update(dt);
if (engineState.fullscreenState.toggleTimer.hasStopped) {
if (isFullscreen) {
rl.ToggleFullscreen();
rl.SetWindowSize(
engineState.fullscreenState.lastWindowWidth,
engineState.fullscreenState.lastWindowHeight,
);
rl.SetWindowPosition(
cast(int) (screenWidth * 0.5f - engineState.fullscreenState.lastWindowWidth * 0.5f),
cast(int) (screenHeight * 0.5f - engineState.fullscreenState.lastWindowHeight * 0.5f),
);
} else {
rl.ToggleFullscreen();
}
}
return result;
}
// Maybe bad idea, but makes life of no-attribute people easier.
_updateFunc = cast(bool function(float _dt) @trusted @nogc nothrow) updateFunc;
engineState.flags.isUpdating = true;
version(WebAssembly) {
static void _updateWindowWeb() {
if (_updateWindow()) {
rl.emscripten_cancel_main_loop();
}
}
rl.emscripten_set_main_loop(&_updateWindowWeb, 0, 1);
} else {
while (true) {
if (rl.WindowShouldClose() || _updateWindow()) {
break;
}
}
}
engineState.flags.isUpdating = false;
}
/// Closes the window.
/// You should avoid calling this function manually.
@trusted
void closeWindow() {
if (!rl.IsWindowReady) return;
engineState.free();
rl.CloseAudioDevice();
rl.CloseWindow();
}
/// Returns true if the drawing is snapped to pixel coordinates.
bool isPixelSnapped() {
return engineState.flags.isPixelSnapped;
}
/// Sets whether drawing should be snapped to pixel coordinates.
void setIsPixelSnapped(bool value) {
engineState.flags.isPixelSnapped = value;
}
/// Toggles whether drawing is snapped to pixel coordinates on or off.
void toggleIsPixelSnapped() {
setIsPixelSnapped(!isPixelSnapped);
}
/// Returns true if the drawing is done in a pixel perfect way.
bool isPixelPerfect() {
return engineState.flags.isPixelPerfect;
}
/// Sets whether drawing should be done in a pixel-perfect way.
void setIsPixelPerfect(bool value) {
engineState.flags.isPixelPerfect = value;
}
/// Toggles the pixel-perfect drawing mode on or off.
void toggleIsPixelPerfect() {
setIsPixelPerfect(!isPixelPerfect);
}
/// Returns true if the cursor is currently visible.
bool isCursorVisible() {
return engineState.flags.isCursorVisible;
}
/// Sets whether the cursor should be visible or hidden.
@trusted
void setIsCursorVisible(bool value) {
engineState.flags.isCursorVisible = value;
if (value) {
rl.ShowCursor();
} else {
rl.HideCursor();
}
}
/// Toggles the visibility of the cursor.
void toggleIsCursorVisible() {
setIsCursorVisible(!isCursorVisible);
}
/// Returns true if the application is currently in fullscreen mode.
@trusted
bool isFullscreen() {
return rl.IsWindowFullscreen();
}
/// Sets whether the application should be in fullscreen mode.
@trusted
void setIsFullscreen(bool value) {
version(WebAssembly) {
} else {
if (value && !isFullscreen) {
engineState.fullscreenState.lastWindowWidth = windowWidth;
engineState.fullscreenState.lastWindowHeight = windowHeight;
rl.SetWindowPosition(0, 0);
rl.SetWindowSize(screenWidth, screenHeight);
engineState.fullscreenState.toggleTimer.start();
} else if (!value && isFullscreen) {
engineState.fullscreenState.toggleTimer.start();
}
}
}
/// Toggles the fullscreen mode on or off.
void toggleIsFullscreen() {
setIsFullscreen(!isFullscreen);
}
/// Returns true if the windows was resized.
@trusted
bool isWindowResized() {
return rl.IsWindowResized();
}
/// Sets the background color to the specified value.
void setBackgroundColor(Color value) {
engineState.viewport.color = value;
}
/// Sets the border color to the specified value.
void setBorderColor(Color value) {
engineState.borderColor = value;
}
/// Returns the default engine font. This font should not be freed.
@trusted
Font engineFont() {
auto result = rl.GetFontDefault().toParin();
result.runeSpacing = 1;
result.lineSpacing = 14;
return result;
}
/// Returns the default filter mode for textures.
Filter defaultFilter() {
return engineState.defaultFilter;
}
/// Returns the default wrap mode for textures.
Wrap defaultWrap() {
return engineState.defaultWrap;
}
/// Sets the default filter mode for textures to the specified value.
void setDefaultFilter(Filter value) {
engineState.defaultFilter = value;
}
/// Sets the default wrap mode for textures to the specified value.
void setDefaultWrap(Wrap value) {
engineState.defaultWrap = value;
}
/// Sets the filter mode used by the engine viewport to the specified value.
void setEngineViewportFilter(Filter value) {
engineState.viewport.setFilter(value);
}
/// Sets the wrap mode used by the engine viewport to the specified value.
void setEngineViewportWrap(Wrap value) {
engineState.viewport.setWrap(value);
}
/// Returns the current master volume level.
@trusted
float masterVolume() {
return rl.GetMasterVolume();
}
/// Sets the master volume level to the specified value.
@trusted
void setMasterVolume(float value) {
rl.SetMasterVolume(value);
}
/// Returns true if the resolution is locked and cannot be changed.
bool isResolutionLocked() {
return !engineState.viewport.isEmpty;
}
/// Locks the resolution to the specified width and height.
@trusted
void lockResolution(int width, int height) {
engineState.viewport.startLocking(width, height);
if (!engineState.flags.isUpdating) {
engineState.viewport.resize(width, height);
}
}
/// Unlocks the resolution, allowing it to be changed.
void unlockResolution() {
engineState.viewport.startUnlocking();
if (!engineState.flags.isUpdating) {
engineState.viewport.free();
}
}
/// Toggles between the current resolution and the specified width and height.
void toggleResolution(int width, int height) {
if (isResolutionLocked) {
unlockResolution();
} else {
lockResolution(width, height);
}
}
/// Returns the current screen width.
@trusted
int screenWidth() {
return rl.GetMonitorWidth(rl.GetCurrentMonitor());
}
/// Returns the current screen height.
@trusted
int screenHeight() {
return rl.GetMonitorHeight(rl.GetCurrentMonitor());
}
/// Returns the current screen size.
Vec2 screenSize() {
return Vec2(screenWidth, screenHeight);
}
/// Returns the current window width.
@trusted
int windowWidth() {
if (isFullscreen) return screenWidth;
else return rl.GetScreenWidth();
}
/// Returns the current window height.
@trusted
int windowHeight() {
if (isFullscreen) return screenHeight;
else return rl.GetScreenHeight();
}
/// Returns the current window size.
Vec2 windowSize() {
return Vec2(windowWidth, windowHeight);
}
/// Returns the current resolution width.
int resolutionWidth() {
if (isResolutionLocked) return engineState.viewport.width;
else return windowWidth;
}
/// Returns the current resolution height.
int resolutionHeight() {
if (isResolutionLocked) return engineState.viewport.height;
else return windowHeight;
}
/// Returns the current resolution size.
Vec2 resolution() {
return Vec2(resolutionWidth, resolutionHeight);
}
/// Returns the current position of the mouse on the screen.
@trusted
Vec2 mouse() {
if (isResolutionLocked) {
auto window = windowSize;
auto minRatio = min(window.x / engineState.viewport.width, window.y / engineState.viewport.height);
if (isPixelPerfect) {
auto roundMinRatio = round(minRatio);
auto floorMinRation = floor(minRatio);
minRatio = minRatio.equals(roundMinRatio, 0.015f) ? roundMinRatio : floorMinRation;
}
auto targetSize = engineState.viewport.size * Vec2(minRatio);
// We use touch because it works on desktop, web and mobile.
return Vec2(
(rl.GetTouchX() - (window.x - targetSize.x) * 0.5f) / minRatio,
(rl.GetTouchY() - (window.y - targetSize.y) * 0.5f) / minRatio,
);
} else {
return Vec2(rl.GetTouchX(), rl.GetTouchY());
}
}
/// Returns the current frames per second (FPS).
@trusted
int fps() {
return rl.GetFPS();
}
/// Returns the total elapsed time since the application started.
@trusted
double elapsedTime() {
return rl.GetTime();
}
/// Returns the total number of ticks elapsed since the application started.
long elapsedTickCount() {
return engineState.tickCount;
}
/// Returns the time elapsed since the last frame.
@trusted
float deltaTime() {
return rl.GetFrameTime();
}
/// Returns the change in mouse position since the last frame.
@trusted
Vec2 deltaMouse() {
return rl.GetMouseDelta().toParin();
}
/// Returns the change in mouse wheel position since the last frame.
@trusted
float deltaWheel() {
auto result = 0.0f;
version (WebAssembly) {
result = -rl.GetMouseWheelMove();
} version (OSX) {
result = -rl.GetMouseWheelMove();
} else {
result = rl.GetMouseWheelMove();
}
if (result < 0.0f) result = -1.0f;
else if (result > 0.0f) result = 1.0f;
else result = 0.0f;
return result;
}
/// Measures the size of the specified text when rendered with the given font and draw options.
@trusted
Vec2 measureTextSize(Font font, IStr text, DrawOptions options = DrawOptions()) {
if (font.isEmpty || text.length == 0) return Vec2();
auto result = Vec2();
auto tempByteCounter = 0; // Used to count longer text line num chars.
auto byteCounter = 0;
auto textWidth = 0.0f;
auto tempTextWidth = 0.0f; // Used to count longer text line width.
auto textHeight = font.size;
auto letter = 0; // Current character.
auto index = 0; // Index position in texture font.
auto i = 0;
while (i < text.length) {
byteCounter += 1;
auto next = 0;
letter = rl.GetCodepointNext(&text[i], &next);
index = rl.GetGlyphIndex(font.data, letter);
i += next;
if (letter != '\n') {
if (font.data.glyphs[index].advanceX != 0) {
textWidth += font.data.glyphs[index].advanceX;
} else {
textWidth += font.data.recs[index].width + font.data.glyphs[index].offsetX;
}
} else {
if (tempTextWidth < textWidth) {
tempTextWidth = textWidth;
}
byteCounter = 0;
textWidth = 0;
textHeight += font.lineSpacing;
}
if (tempByteCounter < byteCounter) {
tempByteCounter = byteCounter;
}
}
if (tempTextWidth < textWidth) {
tempTextWidth = textWidth;
}
result.x = floor(tempTextWidth * options.scale.x + ((tempByteCounter - 1) * font.runeSpacing * options.scale.x));
result.y = floor(textHeight * options.scale.y);
return result;
}
/// Measures the size of the specified text when rendered with the given font and draw options.
Vec2 measureTextSize(FontId font, IStr text, DrawOptions options = DrawOptions()) {
return measureTextSize(font.getOr(), text, options);
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(char key) {
return rl.IsKeyDown(toUpper(key));
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyDown(key) || rl.IsKeyDown(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyDown(key);
}
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Mouse key) {
return rl.IsMouseButtonDown(key);
}
/// Returns true if the specified key is currently pressed.
@trusted
bool isDown(Gamepad key, int id = 0) {
return rl.IsGamepadButtonDown(id, key);
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(char key) {
return rl.IsKeyPressed(toUpper(key));
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyPressed(key) || rl.IsKeyPressed(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyPressed(key);
}
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Mouse key) {
return rl.IsMouseButtonPressed(key);
}
/// Returns true if the specified key was pressed.
@trusted
bool isPressed(Gamepad key, int id = 0) {
return rl.IsGamepadButtonPressed(id, key);
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(char key) {
return rl.IsKeyReleased(toUpper(key));
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Keyboard key) {
if (key == Keyboard.shift) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_SHIFT);
} else if (key == Keyboard.ctrl) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_CONTROL);
} else if (key == Keyboard.alt) {
return rl.IsKeyReleased(key) || rl.IsKeyReleased(rl.KEY_RIGHT_ALT);
} else {
return rl.IsKeyReleased(key);
}
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Mouse key) {
return rl.IsMouseButtonReleased(key);
}
/// Returns true if the specified key was released.
@trusted
bool isReleased(Gamepad key, int id = 0) {
return rl.IsGamepadButtonReleased(id, key);
}
/// Returns the directional input based on the WASD and arrow keys when they are down.
/// The vector is not normalized.
Vec2 wasd() {
auto result = Vec2();
if (Keyboard.w.isDown || Keyboard.up.isDown) result.y -= 1.0f;
if (Keyboard.a.isDown || Keyboard.left.isDown) result.x -= 1.0f;
if (Keyboard.s.isDown || Keyboard.down.isDown) result.y += 1.0f;
if (Keyboard.d.isDown || Keyboard.right.isDown) result.x += 1.0f;
return result;
}
/// Returns the directional input based on the WASD and arrow keys when they are pressed.
/// The vector is not normalized.
Vec2 wasdPressed() {
auto result = Vec2();
if (Keyboard.w.isPressed || Keyboard.up.isPressed) result.y -= 1.0f;
if (Keyboard.a.isPressed || Keyboard.left.isPressed) result.x -= 1.0f;
if (Keyboard.s.isPressed || Keyboard.down.isPressed) result.y += 1.0f;
if (Keyboard.d.isPressed || Keyboard.right.isPressed) result.x += 1.0f;
return result;
}
/// Returns the directional input based on the WASD and arrow keys when they are released.
/// The vector is not normalized.
Vec2 wasdReleased() {
auto result = Vec2();
if (Keyboard.w.isReleased || Keyboard.up.isReleased) result.y -= 1.0f;
if (Keyboard.a.isReleased || Keyboard.left.isReleased) result.x -= 1.0f;
if (Keyboard.s.isReleased || Keyboard.down.isReleased) result.y += 1.0f;
if (Keyboard.d.isReleased || Keyboard.right.isReleased) result.x += 1.0f;
return result;
}
/// Plays the specified sound.
/// The sound will loop automatically for certain file types (OGG, MP3).
@trusted
void playSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (sound.data.isKind!(rl.Sound)) {
rl.PlaySound(sound.data.get!(rl.Sound)());
} else {
rl.PlayMusicStream(sound.data.get!(rl.Music)());
}
}
/// Plays the specified sound.
/// The sound will loop automatically for certain file types (OGG, MP3).
void playSound(SoundId sound) {
playSound(sound.getOr());
}
/// Stops playback of the specified sound.
@trusted
void stopSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (sound.data.isKind!(rl.Sound)) {
rl.StopSound(sound.data.get!(rl.Sound)());
} else {
rl.StopMusicStream(sound.data.get!(rl.Music)());
}
}
/// Stops playback of the specified sound.
void stopSound(SoundId sound) {
stopSound(sound.getOr());
}
/// Pauses playback of the specified sound.
@trusted
void pauseSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (sound.data.isKind!(rl.Sound)) {
rl.PauseSound(sound.data.get!(rl.Sound)());
} else {
rl.PauseMusicStream(sound.data.get!(rl.Music)());
}
}
/// Pauses playback of the specified sound.
void pauseSound(SoundId sound) {
pauseSound(sound.getOr());
}
/// Resumes playback of the specified paused sound.
@trusted
void resumeSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (sound.data.isKind!(rl.Sound)) {
rl.ResumeSound(sound.data.get!(rl.Sound)());
} else {
rl.ResumeMusicStream(sound.data.get!(rl.Music)());
}
}
/// Resumes playback of the specified paused sound.
void resumeSound(SoundId sound) {
resumeSound(sound.getOr());
}
/// Updates the playback state of the specified sound.
@trusted
void updateSound(Sound sound) {
if (sound.isEmpty) {
return;
}
if (sound.data.isKind!(rl.Music)) {
rl.UpdateMusicStream(sound.data.get!(rl.Music)());
}
}
/// Updates the playback state of the specified sound.
void updateSound(SoundId sound) {
updateSound(sound.getOr());
}
/// Draws a rectangle with the specified area and color.
@trusted
void drawRect(Rect area, Color color = white) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawRectanglePro(area.floor().toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
} else {
rl.DrawRectanglePro(area.toRl(), rl.Vector2(0.0f, 0.0f), 0.0f, color.toRl());
}
}
/// Draws a point at the specified location with the given size and color.
void drawVec2(Vec2 point, float size, Color color = white) {
drawRect(Rect(point, size, size).centerArea, color);
}
/// Draws a circle with the specified area and color.
@trusted
void drawCirc(Circ area, Color color = white) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawCircleV(area.position.floor().toRl(), area.radius, color.toRl());
} else {
rl.DrawCircleV(area.position.toRl(), area.radius, color.toRl());
}
}
/// Draws a line with the specified area, thickness, and color.
@trusted
void drawLine(Line area, float size, Color color = white) {
if (isPixelSnapped || isPixelPerfect) {
rl.DrawLineEx(area.a.floor().toRl(), area.b.floor().toRl(), size, color.toRl());
} else {
rl.DrawLineEx(area.a.toRl(), area.b.toRl(), size, color.toRl());
}
}
/// Draws a portion of the specified texture at the given position with the specified draw options.
@trusted
void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
if (texture.isEmpty || area.size.x <= 0.0f || area.size.y <= 0.0f) return;
auto target = Rect(position, area.size * options.scale.abs());
auto flip = options.flip;
if (options.scale.x < 0.0f && options.scale.y < 0.0f) {
flip = oppositeFlip(flip, Flip.xy);
} else if (options.scale.x < 0.0f) {
flip = oppositeFlip(flip, Flip.x);
} else if (options.scale.y < 0.0f) {
flip = oppositeFlip(flip, Flip.y);
}
final switch (flip) {
case Flip.none: break;
case Flip.x: area.size.x *= -1.0f; break;
case Flip.y: area.size.y *= -1.0f; break;
case Flip.xy: area.size *= Vec2(-1.0f); break;
}
auto origin = options.origin == Vec2() ? target.origin(options.hook) : options.origin;
if (isPixelSnapped || isPixelPerfect) {
rl.DrawTexturePro(
texture.data,
area.floor().toRl(),
target.floor().toRl(),
origin.floor().toRl(),
options.rotation,
options.color.toRl(),
);
} else {
rl.DrawTexturePro(
texture.data,
area.toRl(),
target.toRl(),
origin.toRl(),
options.rotation,
options.color.toRl(),
);
}
}
/// Draws a portion of the specified texture at the given position with the specified draw options.
void drawTextureArea(TextureId texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
drawTextureArea(texture.getOr(), area, position, options);
}
/// Draws the texture at the given position with the specified draw options.
void drawTexture(Texture texture, Vec2 position, DrawOptions options = DrawOptions()) {
drawTextureArea(texture, Rect(texture.size), position, options);
}
/// Draws the texture at the given position with the specified draw options.
void drawTexture(TextureId texture, Vec2 position, DrawOptions options = DrawOptions()) {
drawTexture(texture.getOr(), position, options);
}
/// Draws a portion of the specified viewport at the given position with the specified draw options.
void drawViewportArea(Viewport viewport, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
// Some basic rules to make viewports noob friendly.
final switch (options.flip) {
case Flip.none: options.flip = Flip.y; break;
case Flip.x: options.flip = Flip.xy; break;
case Flip.y: options.flip = Flip.none; break;
case Flip.xy: options.flip = Flip.x; break;
}
drawTextureArea(viewport.data.texture.toParin(), area, position, options);
}
/// Draws the viewport at the given position with the specified draw options.
void drawViewport(Viewport viewport, Vec2 position, DrawOptions options = DrawOptions()) {
drawViewportArea(viewport, Rect(viewport.size), position, options);
}
/// Draws a single character from the specified font at the given position with the specified draw options.
@trusted
void drawRune(Font font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) {
if (font.isEmpty) return;
auto rect = toParin(rl.GetGlyphAtlasRec(font.data, rune));
auto origin = options.origin == Vec2() ? rect.origin(options.hook) : options.origin;
rl.rlPushMatrix();
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(position.x.floor(), position.y.floor(), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
rl.rlTranslatef(-origin.x.floor(), -origin.y.floor(), 0.0f);
rl.DrawTextCodepoint(font.data, rune, rl.Vector2(0.0f, 0.0f), font.size, options.color.toRl());
rl.rlPopMatrix();
}
/// Draws a single character from the specified font at the given position with the specified draw options.
void drawRune(FontId font, dchar rune, Vec2 position, DrawOptions options = DrawOptions()) {
drawRune(font.getOr(), rune, position, options);
}
/// Draws the specified text with the given font at the given position using the provided draw options.
@trusted
void drawText(Font font, IStr text, Vec2 position, DrawOptions options = DrawOptions()) {
if (font.isEmpty || text.length == 0) return;
// TODO: Make it work with negative scale values.
auto origin = Rect(measureTextSize(font, text)).origin(options.hook);
rl.rlPushMatrix();
if (isPixelSnapped || isPixelPerfect) {
rl.rlTranslatef(floor(position.x), floor(position.y), 0.0f);
} else {
rl.rlTranslatef(position.x, position.y, 0.0f);
}
rl.rlRotatef(options.rotation, 0.0f, 0.0f, 1.0f);
rl.rlScalef(options.scale.x, options.scale.y, 1.0f);
rl.rlTranslatef(floor(-origin.x), floor(-origin.y), 0.0f);
auto textOffsetY = 0.0f; // Offset between lines (on linebreak '\n').
auto textOffsetX = 0.0f; // Offset X to next character to draw.
auto i = 0;
while (i < text.length) {
// Get next codepoint from byte string and glyph index in font.
auto codepointByteCount = 0;
auto codepoint = rl.GetCodepointNext(&text[i], &codepointByteCount);
auto index = rl.GetGlyphIndex(font.data, codepoint);
if (codepoint == '\n') {
textOffsetY += font.lineSpacing;
textOffsetX = 0.0f;
} else {
if (codepoint != ' ' && codepoint != '\t') {
auto runeOptions = DrawOptions();
runeOptions.color = options.color;
drawRune(font, codepoint, Vec2(textOffsetX, textOffsetY), runeOptions);
}
if (font.data.glyphs[index].advanceX == 0) {
textOffsetX += font.data.recs[index].width + font.runeSpacing;
} else {
textOffsetX += font.data.glyphs[index].advanceX + font.runeSpacing;
}
}
// Move text bytes counter to next codepoint.
i += codepointByteCount;
}
rl.rlPopMatrix();
}
/// Draws text with the given font at the given position using the provided draw options.
void drawText(FontId font, IStr text, Vec2 position, DrawOptions options = DrawOptions()) {
drawText(font.getOr(), text, position, options);
}
/// Draws debug text at the given position with the provided draw options.
void drawDebugText(IStr text, Vec2 position, DrawOptions options = DrawOptions()) {
drawText(engineFont, text, position, options);
}
/// Mixes in a game loop template with specified functions for initialization, update, and cleanup, and sets window size and title.
mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int width = 960, int height = 540, IStr title = "Parin") {
version (D_BetterC) {
extern(C)
void main(int argc, immutable(char)** argv) {
engineEnvArgsBufferLength = argc;
foreach (i; 0 .. argc) {
engineEnvArgsBuffer[i] = argv[i].toStr();
}
openWindow(width, height, argv[0].toStr(), title);
readyFunc();
updateWindow(&updateFunc);
finishFunc();
closeWindow();
}
} else {
void main(string[] args) {
engineEnvArgsBufferLength = args.length;
foreach (i, arg; args) {
engineEnvArgsBuffer[i] = arg;
}
openWindow(width, height, args[0], title);
readyFunc();
updateWindow(&updateFunc);
finishFunc();
closeWindow();
}
}
}