Added JSRClone, audio engine rewrite, bump to alpha.2

This commit is contained in:
Natty 2022-04-27 19:24:18 +02:00
parent 0a3026e3d9
commit 1aaee9b17a
No known key found for this signature in database
GPG Key ID: 40AB22FA416C7019
130 changed files with 4126 additions and 528 deletions

View File

@ -1,50 +0,0 @@
## Features targeted for 22.3.0.0-alpha.0
* `[PlutoRuntime,PlutoCore]` **Initial implementation of the layer system (formerly known as "stage")**
* A "layer", in this context, is a set of assets bound together
by programming logic.
Layer switching and asset management are handled by the engine.
* Layers can be stacked on top of each other and run sequentially
from bottom to top
* Upon layer switch
1. Unload unused assets
2. Load new assets
* Provide multiple means of layer switching
* Two modes with the initial release, asynchronous switching will come at a later date
1. Instant switch
* Assets will be loaded and unloaded synchronously
* The layer switch will happen in one frame
2. Deferred switch
* The layer will continue running until all assets load
* Assets will load synchronously, but at a slower pace
to avoid frame stutter
* Automated asset loading
* All asset management will eventually be handled by `PlutoCore`
* This includes audio clips, textures, sprites
* Add a common interface for all assets
* Let the stage system handle audio playback
* This API should be as neutral as possible and avoid steering towards
game-only use
* The stage manager should be relatively low-overhead and allow multiple
instances
* Allow stages to be inherited from, creating a stack-like structure
* `[PlutoAudio]` Integrate the Audio API with the Stage API
## Features targeted for an unspecified future release
* `[PlutoSpritesheet]` Expanded capabilities
* Support for 9-slice rendering
* Support for animated sprite rendering
* Support for multidirectional sprite rendering
* A spritesheet skeleton editor
* `[PlutoRuntime]`
* Asynchronous switch
* Assets will be loaded in asynchronously, where applicable
* Falls back to deferred switching for synchronous loading,
such as OpenGL texture upload
* `[PlutoGUI]` A fully-fledged GUI engine
* Improve font-rendering capabilities
* Subpixel rendering support [?]
* Reimplement support for bitmap fonts
* Improve upon the support of thread-local Pluto instances
* The long term goal is to allow an unlimited amount of Pluto applications at any given time

View File

@ -25,7 +25,7 @@ repositories {
dependencies {
implementation group: "org.plutoengine", name: "plutocore", version: "22.2.0.0-alpha.0"
implementation group: "org.plutoengine", name: "plutocore", version: "22.2.0.0-alpha.2"
}
```
@ -41,10 +41,17 @@ repositories {
}
dependencies {
implementation("org.plutoengine", "plutocore", "22.2.0.0-alpha.0")
implementation("org.plutoengine", "plutocore", "22.2.0.0-alpha.2")
}
```
### Licensing
While all code of PlutoEngine is licensed under MIT, some media in this repository
is licensed under different terms:
* Music in the **jsr-clone** demo is made by Selulance, licensed under **CC - BY 3.0**
### Versioning
All submodules share a version number for simplicity reasons.
@ -64,6 +71,9 @@ version numbers.*
## Usability status of submodules
Keep in mind PlutoEngine is in alpha and all features are tentative.
The following list simply provides an overview of how likely breaking changes are to occur.
### Safe submodules
* **PlutoCore** - Stable
* **PlutoFramebuffer** - Stable
@ -74,11 +84,11 @@ version numbers.*
* **PlutoDisplay** - Stable, collision API nowhere near completion
* **PlutoUSS2** - Stable
* **PlutoLib** - Mostly stable
* **PlutoRuntime** - Mostly stable
### Unstable submodules
* **PlutoGUI** - Recently rewritten, the API is highly unstable
* **PlutoRuntime** - Somewhat tentative, the module API has been rewritten and might contain bugs
* **PlutoAudio** - Somewhat usable, unfinished
* **PlutoAudio** - Very tentative, work in progress
* **PlutoGUI** - Recently rewritten, the API is highly unstable, work in progress
## Current priorities
@ -92,12 +102,11 @@ See `NEXT_RELEASE_DRAFT.md` for details.
### Very high priority
[ *Implemented in the current release.* ]
* Implement the layer system and integrate all existing systems with it
* Improve image loading capabilities, possibly rewrite PlutoLib#TPL
### High priority
[ *Implemented in the next release.* ]
* Finish PlutoAudio
* Depends on the stage system
* Expand upon the Color API
* Color mixing and blending
* Color transformation

View File

@ -1,4 +1,8 @@
## 22.2.0.0-alpha.2
* `[SDK]` The libraries now always reference natives for all architectures
* `[SDK]` Replaced `NEXT_RELEASE_DRAFT.md` with [an issue tracker](https://github.com/493msi/plutoengine/issues)
* `[PlutoAudio]` **Partial rewrite and support for managed sound effects**
* `plutoengine-demos/` **Added the `jsr-clone` demo**
* `[PlutoSpritesheet]` Renamed `TemporalSprite#getSideCount` to `getFrameCount`
## 22.2.0.0-alpha.1

View File

@ -5,11 +5,16 @@ import org.gradle.api.JavaVersion
object Versions {
const val lwjglVersion = "3.3.1"
val lwjglNatives = when (OperatingSystem.current()) {
OperatingSystem.LINUX -> "natives-linux"
OperatingSystem.WINDOWS -> "natives-windows"
else -> throw Error("Unsupported operating system!")
}
val lwjglNatives = listOf(
"natives-linux-arm64",
"natives-linux-arm32",
"natives-linux",
"natives-macos-arm64",
"natives-macos",
"natives-windows-arm64",
"natives-windows",
"natives-windows-x86"
)
const val jomlVersion = "1.10.2"
const val jomlPrimitivesVersion = "1.10.0"
@ -23,7 +28,7 @@ object Versions {
const val isPrerelease = true
const val prereleaseName = "alpha"
const val prerealeaseUpdate = 1
const val prerealeaseUpdate = 2
val versionFull =
if (isPrerelease)

View File

@ -12,5 +12,7 @@ dependencies {
api("org.lwjgl:lwjgl-openal")
runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = Versions.lwjglNatives)
org.plutoengine.Versions.lwjglNatives.forEach {
runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = it)
}
}

View File

@ -1,8 +1,5 @@
package org.plutoengine.audio;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.buffer.BufferHelper;
import org.plutoengine.logger.Logger;
@ -10,8 +7,6 @@ import org.plutoengine.logger.SmartSeverity;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
@ -23,7 +18,7 @@ public class AudioLoader
* medium-sized audio files, however it is discouraged to use such a track
* in multiple audio sources at once due to the cost of seeking.
*/
public static ISeekableAudioTrack loadMemoryDecoded(Path path)
public static SeekableTrack loadMemoryDecoded(Path path)
{
Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", path);
@ -45,13 +40,13 @@ public class AudioLoader
* for from-memory PCM streaming. Good for frequently used small audio
* files.
*/
public static ISeekableAudioTrack loadMemoryPCM(Path path)
public static RandomAccessClip loadMemoryPCM(Path path)
{
Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", path);
try
{
return new MemoryPCMTrack(path);
return new MemoryPCMClip(path);
}
catch (IOException e)
{
@ -61,7 +56,7 @@ public class AudioLoader
}
}
private static ByteBuffer loadIntoMemory(Path path) throws IOException
static ByteBuffer loadIntoMemory(Path path) throws IOException
{
var size = Files.size(path);
@ -73,202 +68,4 @@ public class AudioLoader
return BufferHelper.readToByteBuffer(path, readData);
}
private abstract static class StreamableTrack implements IAudioStream
{
protected int channels;
protected int sampleRate;
@Override
public int getChannels()
{
return this.channels;
}
@Override
public int getSampleRate()
{
return this.sampleRate;
}
}
private abstract static class SeekableTrack extends StreamableTrack implements ISeekableAudioTrack
{
protected int samplesLength;
@Override
public int getLengthInSamples()
{
return this.samplesLength;
}
}
public static class MemoryPCMTrack extends SeekableTrack
{
private final ShortBuffer pcmAudio;
private int sampleOffset = 0;
private MemoryPCMTrack(Path path) throws IOException
{
long handle = MemoryUtil.NULL;
ByteBuffer audioBytes = null;
try (MemoryStack stack = MemoryStack.stackPush())
{
audioBytes = loadIntoMemory(path);
IntBuffer error = stack.mallocInt(1);
handle = STBVorbis.stb_vorbis_open_memory(audioBytes, error, null);
if (handle == MemoryUtil.NULL)
{
this.close();
throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.malloc(stack);
STBVorbis.stb_vorbis_get_info(handle, info);
this.channels = info.channels();
this.sampleRate = info.sample_rate();
// Downmix to mono, SOUNDS HORRIBLE
//
// this.channels = 1;
// this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels;
// this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength);
// var ptr = stack.pointers(this.pcmAudio);
// STBVorbis.stb_vorbis_get_samples_short(handle, ptr, this.samplesLength);
this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels;
this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength);
STBVorbis.stb_vorbis_get_samples_short_interleaved(handle, this.channels, this.pcmAudio);
}
catch (IOException e)
{
this.close();
throw e;
}
finally
{
MemoryUtil.memFree(audioBytes);
if (handle != MemoryUtil.NULL)
{
STBVorbis.stb_vorbis_close(handle);
}
}
}
@Override
public void seek(int sampleIndex)
{
this.sampleOffset = sampleIndex * this.getChannels();
}
@Override
public synchronized int getSamples(ShortBuffer pcm)
{
this.pcmAudio.limit(Math.min(this.sampleOffset + pcm.remaining(), this.pcmAudio.capacity()));
int read = this.pcmAudio.remaining();
pcm.put(this.pcmAudio);
this.sampleOffset += read;
pcm.clear();
return read / this.getChannels();
}
@Override
public int getSampleOffset()
{
return this.sampleOffset / this.getChannels();
}
@Override
public void close()
{
MemoryUtil.memFree(this.pcmAudio);
}
}
public static class MemoryDecodedVorbisTrack extends SeekableTrack
{
protected long handle;
private final ByteBuffer encodedAudio;
private MemoryDecodedVorbisTrack(Path path) throws IOException
{
try
{
this.encodedAudio = loadIntoMemory(path);
}
catch (IOException e)
{
this.close();
throw e;
}
try (MemoryStack stack = MemoryStack.stackPush())
{
IntBuffer error = stack.mallocInt(1);
this.handle = STBVorbis.stb_vorbis_open_memory(this.encodedAudio, error, null);
if (this.handle == MemoryUtil.NULL)
{
this.close();
throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.malloc(stack);
STBVorbis.stb_vorbis_get_info(this.handle, info);
this.channels = info.channels();
this.sampleRate = info.sample_rate();
}
this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(this.handle);
Logger.logf(SmartSeverity.AUDIO, """
\tSample rate:\t%d
\t\tChannels:\t%d
\t\tSamples:\t%d
%n""", this.sampleRate, this.channels, this.samplesLength);
}
@Override
public synchronized int getSamples(ShortBuffer pcm)
{
if (this.handle == MemoryUtil.NULL)
{
return -1;
}
return STBVorbis.stb_vorbis_get_samples_short_interleaved(this.handle, this.getChannels(), pcm);
}
@Override
public void close()
{
MemoryUtil.memFree(this.encodedAudio);
if (this.handle != MemoryUtil.NULL)
{
STBVorbis.stb_vorbis_close(this.handle);
this.handle = MemoryUtil.NULL;
}
}
@Override
public synchronized void seek(int sampleIndex)
{
STBVorbis.stb_vorbis_seek(this.handle, sampleIndex);
}
@Override
public int getSampleOffset()
{
return STBVorbis.stb_vorbis_get_sample_offset(this.handle);
}
}
}

View File

@ -0,0 +1,12 @@
package org.plutoengine.audio;
public abstract class ClipTrack extends SeekableTrack implements ISeekableClip
{
protected int samplesLength;
@Override
public int getLengthInSamples()
{
return this.samplesLength;
}
}

View File

@ -0,0 +1,10 @@
package org.plutoengine.audio;
public interface IAudio extends AutoCloseable
{
int getSampleRate();
int getChannels();
void close();
}

View File

@ -1,14 +0,0 @@
package org.plutoengine.audio;
import java.nio.ShortBuffer;
public interface IAudioStream extends AutoCloseable
{
int getSamples(ShortBuffer pcm);
int getSampleRate();
int getChannels();
void close();
}

View File

@ -0,0 +1,6 @@
package org.plutoengine.audio;
public interface IClip extends IAudio
{
int getLengthInSamples();
}

View File

@ -0,0 +1,8 @@
package org.plutoengine.audio;
import java.nio.ShortBuffer;
public interface IRandomAccessAudio extends IClip
{
int getSamples(ShortBuffer pcm, int offset, boolean loopRead);
}

View File

@ -1,11 +1,7 @@
package org.plutoengine.audio;
public interface ISeekableAudioTrack extends IAudioStream
public interface ISeekableClip extends ISeekableTrack, IClip
{
int getSampleOffset();
int getLengthInSamples();
default void skip(int sampleCount)
{
this.seek(Math.min(Math.max(0, this.getSampleOffset() + sampleCount), this.getLengthInSamples()));
@ -15,11 +11,4 @@ public interface ISeekableAudioTrack extends IAudioStream
{
this.seek(Math.round(this.getLengthInSamples() * offset0to1));
}
default void rewind()
{
this.seek(0);
}
void seek(int sampleIndex);
}

View File

@ -0,0 +1,11 @@
package org.plutoengine.audio;
public interface ISeekableTrack extends IStreamingAudio
{
void seek(int sampleIndex);
default void rewind()
{
this.seek(0);
}
}

View File

@ -0,0 +1,10 @@
package org.plutoengine.audio;
import java.nio.ShortBuffer;
public interface IStreamingAudio extends IAudio
{
int getSamples(ShortBuffer pcm);
int getSampleOffset();
}

View File

@ -0,0 +1,95 @@
package org.plutoengine.audio;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.nio.file.Path;
public class MemoryDecodedVorbisTrack extends ClipTrack
{
protected long handle;
private final ByteBuffer encodedAudio;
MemoryDecodedVorbisTrack(Path path) throws IOException
{
try
{
this.encodedAudio = AudioLoader.loadIntoMemory(path);
}
catch (IOException e)
{
this.close();
throw e;
}
try (MemoryStack stack = MemoryStack.stackPush())
{
IntBuffer error = stack.mallocInt(1);
this.handle = STBVorbis.stb_vorbis_open_memory(this.encodedAudio, error, null);
if (this.handle == MemoryUtil.NULL)
{
this.close();
throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.malloc(stack);
STBVorbis.stb_vorbis_get_info(this.handle, info);
this.channels = info.channels();
this.sampleRate = info.sample_rate();
}
this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(this.handle);
Logger.logf(SmartSeverity.AUDIO, """
\tSample rate:\t%d
\t\tChannels:\t%d
\t\tSamples:\t%d
%n""", this.sampleRate, this.channels, this.samplesLength);
}
@Override
public synchronized int getSamples(ShortBuffer pcm)
{
if (this.handle == MemoryUtil.NULL)
{
return -1;
}
return STBVorbis.stb_vorbis_get_samples_short_interleaved(this.handle, this.getChannels(), pcm);
}
@Override
public void close()
{
MemoryUtil.memFree(this.encodedAudio);
if (this.handle != MemoryUtil.NULL)
{
STBVorbis.stb_vorbis_close(this.handle);
this.handle = MemoryUtil.NULL;
}
}
@Override
public synchronized void seek(int sampleIndex)
{
STBVorbis.stb_vorbis_seek(this.handle, sampleIndex);
}
@Override
public int getSampleOffset()
{
return STBVorbis.stb_vorbis_get_sample_offset(this.handle);
}
}

View File

@ -0,0 +1,101 @@
package org.plutoengine.audio;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.nio.file.Path;
public class MemoryPCMClip extends RandomAccessClip
{
private final ShortBuffer pcmAudio;
MemoryPCMClip(Path path) throws IOException
{
long handle = MemoryUtil.NULL;
ByteBuffer audioBytes = null;
try (MemoryStack stack = MemoryStack.stackPush())
{
audioBytes = AudioLoader.loadIntoMemory(path);
IntBuffer error = stack.mallocInt(1);
handle = STBVorbis.stb_vorbis_open_memory(audioBytes, error, null);
if (handle == MemoryUtil.NULL)
{
this.close();
throw new IOException(String.format("Failed to load '%s', error code %d.\n", path, error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.malloc(stack);
STBVorbis.stb_vorbis_get_info(handle, info);
this.channels = info.channels();
this.sampleRate = info.sample_rate();
// Downmix to mono, SOUNDS HORRIBLE
//
// this.channels = 1;
// this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels;
// this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength);
// var ptr = stack.pointers(this.pcmAudio);
// STBVorbis.stb_vorbis_get_samples_short(handle, ptr, this.samplesLength);
this.samplesLength = STBVorbis.stb_vorbis_stream_length_in_samples(handle) * this.channels;
this.pcmAudio = MemoryUtil.memAllocShort(this.samplesLength);
STBVorbis.stb_vorbis_get_samples_short_interleaved(handle, this.channels, this.pcmAudio);
}
catch (IOException e)
{
this.close();
throw e;
}
finally
{
MemoryUtil.memFree(audioBytes);
if (handle != MemoryUtil.NULL)
{
STBVorbis.stb_vorbis_close(handle);
}
}
}
@Override
public int getLengthInSamples()
{
return this.samplesLength / this.getChannels();
}
@Override
public int getSamples(ShortBuffer pcm, int offset, boolean loopRead)
{
int readTotal = 0;
int read;
do
{
int thisRemaining = this.pcmAudio.limit() - offset;
read = Math.min(pcm.remaining() - readTotal, thisRemaining);
pcm.put(pcm.position() + readTotal, this.pcmAudio, offset, read);
readTotal += read;
offset = (offset + read) % (this.getLengthInSamples() * this.channels);
}
while (loopRead && pcm.limit() - readTotal > 0);
return readTotal / this.getChannels();
}
@Override
public void close()
{
MemoryUtil.memFree(this.pcmAudio);
}
}

View File

@ -0,0 +1,12 @@
package org.plutoengine.audio;
public abstract class RandomAccessClip extends Track implements IRandomAccessAudio, IClip
{
protected int samplesLength;
@Override
public int getLengthInSamples()
{
return this.samplesLength;
}
}

View File

@ -0,0 +1,5 @@
package org.plutoengine.audio;
public abstract class SeekableTrack extends Track implements ISeekableTrack
{
}

View File

@ -0,0 +1,19 @@
package org.plutoengine.audio;
abstract class Track implements IAudio
{
protected int channels;
protected int sampleRate;
@Override
public int getChannels()
{
return this.channels;
}
@Override
public int getSampleRate()
{
return this.sampleRate;
}
}

View File

@ -0,0 +1,35 @@
package org.plutoengine.audio.al;
import org.lwjgl.openal.AL10;
import java.nio.ShortBuffer;
class AudioBuffer implements AutoCloseable
{
private final int id;
private final int format;
private final int sampleRate;
AudioBuffer(int id, int format, int sampleRate)
{
this.id = id;
this.format = format;
this.sampleRate = sampleRate;
}
public int getID()
{
return this.id;
}
public void writeData(ShortBuffer data)
{
AL10.alBufferData(this.id, this.format, data, this.sampleRate);
}
@Override
public void close()
{
AL10.alDeleteBuffers(this.id);
}
}

View File

@ -0,0 +1,44 @@
package org.plutoengine.audio.al;
import org.plutoengine.audio.RandomAccessClip;
import java.nio.ShortBuffer;
public class AudioClipSource extends AudioDoubleBufferedSource
{
private final RandomAccessClip clip;
private final boolean looping;
private int readHead = 0;
public AudioClipSource(RandomAccessClip clip, boolean looping)
{
super(clip);
this.clip = clip;
this.looping = looping;
}
public AudioClipSource(RandomAccessClip clip)
{
this(clip, false);
}
@Override
protected int getSamples(ShortBuffer pcmTransferBuf)
{
var read = this.clip.getSamples(pcmTransferBuf, this.readHead * this.channels, this.looping);
this.readHead += read / this.channels;
if (this.looping)
this.readHead %= this.clip.getLengthInSamples();
return read;
}
public boolean isLooping()
{
return this.looping;
}
}

View File

@ -0,0 +1,152 @@
package org.plutoengine.audio.al;
import org.joml.Matrix4x3f;
import org.joml.Matrix4x3fc;
import org.joml.Vector3f;
import org.joml.Vector3fc;
import org.lwjgl.openal.*;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.Pluto;
import org.plutoengine.component.PlutoLocalComponent;
import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
/**
* @author 493msi
*
*/
public class AudioContext extends PlutoLocalComponent
{
private long device = MemoryUtil.NULL;
private long context = MemoryUtil.NULL;
private ALCapabilities capabilities;
private Matrix4x3fc transformation;
AudioContext()
{
this.transformation = new Matrix4x3f();
}
@Override
protected void onMount(ComponentDependencyManager manager)
{
var devicePtr = ALC10.alcOpenDevice((ByteBuffer) null);
if (devicePtr == MemoryUtil.NULL)
{
Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to open the default audio device.");
// No audio device found, but the game should not crash just because we have no audio
return;
}
this.device = devicePtr;
var contextPtr = ALC10.alcCreateContext(devicePtr, (IntBuffer) null);
if (contextPtr == MemoryUtil.NULL)
{
ALC10.alcCloseDevice(devicePtr);
Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to create an OpenAL context.");
// The game should not crash just because we have no audio
return;
}
this.context = contextPtr;
EXTThreadLocalContext.alcSetThreadContext(contextPtr);
ALCCapabilities deviceCaps = ALC.createCapabilities(devicePtr);
var alCapabilities = AL.createCapabilities(deviceCaps);
if (Pluto.DEBUG_MODE)
{
Logger.logf(SmartSeverity.AUDIO, "OpenAL10: %b\n", alCapabilities.OpenAL10);
Logger.logf(SmartSeverity.AUDIO, "OpenAL11: %b\n", alCapabilities.OpenAL11);
Logger.logf(SmartSeverity.AUDIO, "Distance model: %s\n", EnumDistanceModel.getByID(AL11.alGetInteger(AL11.AL_DISTANCE_MODEL)));
}
this.capabilities = alCapabilities;
Logger.log(SmartSeverity.AUDIO_PLUS, "Audio engine started.");
}
public void setTransformation(Matrix4x3fc transformation)
{
this.transformation = transformation;
}
public Vector3fc transform(Vector3fc vector)
{
return this.transformation.transformPosition(vector, new Vector3f());
}
public void setDistanceModel(EnumDistanceModel model)
{
AL10.alDistanceModel(model.getALID());
}
public void setSpeedOfSound(float speedOfSound)
{
AL11.alSpeedOfSound(speedOfSound);
}
public void setSpeed(Vector3fc speed)
{
var tSpeed = this.transformation.transformPosition(speed, new Vector3f());
AL10.alListener3f(AL10.AL_VELOCITY, tSpeed.x(), tSpeed.y(), tSpeed.z());
}
public void setPosition(Vector3f position)
{
var tPosition = this.transformation.transformPosition(position, new Vector3f());
AL10.alListener3f(AL10.AL_POSITION, tPosition.x(), tPosition.y(), tPosition.z());
}
public void setVolume(float volume)
{
AL10.alListenerf(AL10.AL_GAIN, volume);
}
public void setOrientation(Vector3f at, Vector3f up)
{
float[] data = new float[6];
data[0] = at.x;
data[1] = at.y;
data[2] = at.z;
data[3] = up.x;
data[4] = up.y;
data[5] = up.z;
AL10.alListenerfv(AL10.AL_ORIENTATION, data);
}
public boolean isReady()
{
return this.capabilities != null;
}
@Override
protected void onUnmount()
{
Logger.log(SmartSeverity.AUDIO_MINUS, "Shutting down the audio engine.");
EXTThreadLocalContext.alcSetThreadContext(MemoryUtil.NULL);
ALC10.alcDestroyContext(this.context);
ALC10.alcCloseDevice(this.device);
this.context = MemoryUtil.NULL;
this.device = MemoryUtil.NULL;
this.capabilities = null;
}
@Override
public boolean isUnique()
{
return true;
}
}

View File

@ -0,0 +1,185 @@
package org.plutoengine.audio.al;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.SOFTDirectChannels;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.audio.IAudio;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
abstract class AudioDoubleBufferedSource extends AudioSource
{
private static final int BUFFER_SIZE_PER_CHANNEL = 8192;
private static final int DOUBLE_BUFFER = 2;
protected final int format;
protected final int channels;
protected final int sampleRate;
private final IntBuffer bufferIDs;
protected final Map<Integer, AudioBuffer> buffers;
protected final ShortBuffer pcmTransferBuf;
protected boolean audioBufferDepleted;
protected boolean closed;
protected AudioDoubleBufferedSource(IAudio audio)
{
this.format = switch (audio.getChannels()) {
case 1 -> AL10.AL_FORMAT_MONO16;
case 2 -> AL10.AL_FORMAT_STEREO16;
default -> throw new UnsupportedOperationException("Unsupported number of channels: " + audio.getChannels());
};
this.channels = audio.getChannels();
this.sampleRate = audio.getSampleRate();
int bufferSize = audio.getChannels() * BUFFER_SIZE_PER_CHANNEL;
this.pcmTransferBuf = MemoryUtil.memAllocShort(bufferSize);
this.bufferIDs = MemoryUtil.memCallocInt(DOUBLE_BUFFER);
AL10.alGenBuffers(this.bufferIDs);
this.buffers = IntStream.range(0, this.bufferIDs.limit())
.mapToObj(i -> new AudioBuffer(this.bufferIDs.get(i), this.format, this.sampleRate))
.collect(Collectors.toMap(AudioBuffer::getID, Function.identity(), (l, r) -> l));
AL10.alSourcei(this.id, SOFTDirectChannels.AL_DIRECT_CHANNELS_SOFT, AL10.AL_TRUE);
}
public boolean play()
{
if (this.closed)
return false;
var state = AL10.alGetSourcei(this.id, AL10.AL_SOURCE_STATE);
return switch (state)
{
case AL10.AL_PLAYING -> true;
case AL10.AL_PAUSED, AL10.AL_STOPPED -> super.play();
case AL10.AL_INITIAL -> {
this.buffers.values()
.forEach(this::stream);
yield super.play();
}
default -> false; // Unexpected value, just say it didn't play
};
}
private void stream(AudioBuffer buffer)
{
if (this.audioBufferDepleted)
return;
this.fillTransferBuffer();
if (this.audioBufferDepleted)
return;
buffer.writeData(this.pcmTransferBuf);
AL10.alSourceQueueBuffers(this.id, buffer.getID());
}
private void fillTransferBuffer()
{
this.pcmTransferBuf.clear();
int samplesPerChannel = this.getSamples(this.pcmTransferBuf);
if (samplesPerChannel < BUFFER_SIZE_PER_CHANNEL)
{
this.audioBufferDepleted = true;
return;
}
var samples = samplesPerChannel * this.channels;
this.pcmTransferBuf.limit(samples);
}
protected abstract int getSamples(ShortBuffer pcmTransferBuf);
protected List<AudioBuffer> unqueueBuffers()
{
int processed = AL10.alGetSourcei(this.id, AL10.AL_BUFFERS_PROCESSED);
var unqueued = new ArrayList<AudioBuffer>(DOUBLE_BUFFER);
for (int i = 0; i < processed; i++)
{
int bufID = AL10.alSourceUnqueueBuffers(this.id);
var buffer = this.buffers.get(bufID);
unqueued.add(buffer);
}
return unqueued;
}
public boolean update()
{
if (this.isClosed())
return false;
var unqueued = this.unqueueBuffers();
unqueued.forEach(this::stream);
if (AL10.alGetSourcei(this.id, AL10.AL_SOURCE_STATE) == AL10.AL_STOPPED)
{
if (this.audioBufferDepleted)
return false;
if (unqueued.size() == DOUBLE_BUFFER)
return super.play();
return false;
}
return true;
}
public boolean updateOrClose()
{
boolean shouldClose = !this.update();
if (shouldClose)
this.close();
return shouldClose;
}
public boolean isClosed()
{
return this.closed;
}
@Override
public void close()
{
if (this.isClosed())
return;
this.closed = true;
this.stop();
this.unqueueBuffers();
super.close();
this.buffers.values()
.forEach(AudioBuffer::close);
MemoryUtil.memFree(this.pcmTransferBuf);
}
}

View File

@ -1,122 +1,78 @@
package org.plutoengine.audio.al;
import org.apache.commons.lang3.tuple.Pair;
import org.joml.Vector3f;
import org.lwjgl.openal.*;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.Pluto;
import org.plutoengine.component.AbstractComponent;
import org.plutoengine.component.ComponentToken;
import org.plutoengine.component.PlutoLocalComponent;
import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* @author 493msi
*
*/
public class AudioEngine extends PlutoLocalComponent
{
private long device = MemoryUtil.NULL;
private long context = MemoryUtil.NULL;
private ALCapabilities capabilities;
public static final ComponentToken<AudioEngine> TOKEN = ComponentToken.create(AudioEngine::new);
private AudioContext context;
private final List<Pair<AudioClipSource, AudioSourceInfo>> sfx;
private AudioEngine()
{
this.sfx = new ArrayList<>();
}
@Override
protected void onMount(ComponentDependencyManager manager)
protected void onMount(AbstractComponent<PlutoLocalComponent>.ComponentDependencyManager manager)
{
var devicePtr = ALC10.alcOpenDevice((ByteBuffer) null);
if (devicePtr == MemoryUtil.NULL)
{
Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to open the default audio device.");
// No audio device found, but the game should not crash just because we have no audio
return;
this.context = manager.declareDependency(ComponentToken.create(AudioContext::new));
}
this.device = devicePtr;
var contextPtr = ALC10.alcCreateContext(devicePtr, (IntBuffer) null);
if (contextPtr == MemoryUtil.NULL)
public void update()
{
ALC10.alcCloseDevice(devicePtr);
for (var iterator = this.sfx.listIterator(); iterator.hasNext(); )
{
var data = iterator.next();
var source = data.getKey();
var info = data.getValue();
Logger.log(SmartSeverity.AUDIO_ERROR, "Failed to create an OpenAL context.");
var kaFunc = info.keepAliveFunction();
if (kaFunc != null && !kaFunc.getAsBoolean())
source.close();
// The game should not crash just because we have no audio
return;
var moveFunc = info.moveFunction();
if (!source.isClosed() && moveFunc != null)
{
var prevPos = source.getPosition();
var newPos = moveFunc.apply(prevPos);
var velocity = newPos.sub(prevPos, new Vector3f());
source.position(this.context, newPos);
source.velocity(this.context, velocity);
}
this.context = contextPtr;
EXTThreadLocalContext.alcSetThreadContext(contextPtr);
ALCCapabilities deviceCaps = ALC.createCapabilities(devicePtr);
var alCapabilities = AL.createCapabilities(deviceCaps);
if (Pluto.DEBUG_MODE)
{
Logger.logf(SmartSeverity.AUDIO, "OpenAL10: %b\n", alCapabilities.OpenAL10);
Logger.logf(SmartSeverity.AUDIO, "OpenAL11: %b\n", alCapabilities.OpenAL11);
if (source.updateOrClose())
iterator.remove();
}
}
this.capabilities = alCapabilities;
Logger.log(SmartSeverity.AUDIO_PLUS, "Audio engine started.");
}
public void setSpeed(Vector3f speed)
public void playSound(SoundEffect sfx)
{
AL10.alListener3f(AL10.AL_VELOCITY, speed.x, speed.y, speed.z);
}
public void setPosition(Vector3f position)
{
AL10.alListener3f(AL10.AL_POSITION, position.x, position.y, position.z);
}
public void setVolume(float volume)
{
AL10.alListenerf(AL10.AL_GAIN, volume);
}
public void setOrientation(Vector3f at, Vector3f up)
{
float[] data = new float[6];
data[0] = at.x;
data[1] = at.y;
data[2] = at.z;
data[3] = up.x;
data[4] = up.y;
data[5] = up.z;
AL10.alListenerfv(AL10.AL_ORIENTATION, data);
}
public boolean isReady()
{
return this.capabilities != null;
var soundEffect = new AudioClipSource(sfx.getClip());
soundEffect.volume(sfx.getVolume());
soundEffect.pitch(sfx.getPitch());
soundEffect.position(this.context, sfx.getPosition());
soundEffect.play();
var info = new AudioSourceInfo(sfx.getMovementMapper(), sfx.getKeepAliveFunction());
var data = Pair.of(soundEffect, info);
this.sfx.add(data);
}
@Override
protected void onUnmount()
{
Logger.log(SmartSeverity.AUDIO_MINUS, "Shutting down the audio engine.");
EXTThreadLocalContext.alcSetThreadContext(MemoryUtil.NULL);
ALC10.alcDestroyContext(this.context);
ALC10.alcCloseDevice(this.device);
this.context = MemoryUtil.NULL;
this.device = MemoryUtil.NULL;
this.capabilities = null;
}
@Override
@ -124,4 +80,9 @@ public class AudioEngine extends PlutoLocalComponent
{
return true;
}
public AudioContext getContext()
{
return this.context;
}
}

View File

@ -1,45 +1,73 @@
package org.plutoengine.audio.al;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import org.joml.Vector3fc;
import org.lwjgl.openal.AL10;
public abstract class AudioSource implements AutoCloseable
{
protected final int source;
protected final int id;
protected Vector3fc position;
protected AudioSource()
{
this.source = AL10.alGenSources();
this.id = AL10.alGenSources();
}
@MustBeInvokedByOverriders
public boolean play()
{
AL10.alSourcePlay(this.id);
return true;
}
@MustBeInvokedByOverriders
public void pause()
{
AL10.alSourcePause(this.id);
}
@MustBeInvokedByOverriders
public void stop()
{
AL10.alSourceStop(this.source);
AL10.alSourceStop(this.id);
}
@MustBeInvokedByOverriders
public void close()
{
this.stop();
AL10.alDeleteSources(this.source);
AL10.alDeleteSources(this.id);
}
public void position(Vector3fc pos)
@MustBeInvokedByOverriders
public void position(AudioContext context, Vector3fc pos)
{
AL10.alSource3f(this.source, AL10.AL_POSITION, pos.x(), pos.y(), pos.z());
this.position = pos;
var tPos = context.transform(pos);
AL10.alSource3f(this.id, AL10.AL_POSITION, tPos.x(), tPos.y(), tPos.z());
}
public void velocity(Vector3fc velocity)
public Vector3fc getPosition()
{
AL10.alSource3f(this.source, AL10.AL_VELOCITY, velocity.x(), velocity.y(), velocity.z());
return this.position;
}
@MustBeInvokedByOverriders
public void velocity(AudioContext context, Vector3fc velocity)
{
var tVelocity = context.transform(velocity);
AL10.alSource3f(this.id, AL10.AL_VELOCITY, tVelocity.x(), tVelocity.y(), tVelocity.z());
}
@MustBeInvokedByOverriders
public void pitch(float f)
{
AL10.alSourcef(this.source, AL10.AL_PITCH, f);
AL10.alSourcef(this.id, AL10.AL_PITCH, f);
}
@MustBeInvokedByOverriders
public void volume(float f)
{
AL10.alSourcef(this.source, AL10.AL_GAIN, f);
AL10.alSourcef(this.id, AL10.AL_GAIN, f);
}
}

View File

@ -0,0 +1,13 @@
package org.plutoengine.audio.al;
import org.joml.Vector3fc;
import java.util.function.BooleanSupplier;
import java.util.function.UnaryOperator;
public record AudioSourceInfo(
UnaryOperator<Vector3fc> moveFunction,
BooleanSupplier keepAliveFunction
)
{
}

View File

@ -1,113 +0,0 @@
package org.plutoengine.audio.al;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.SOFTDirectChannels;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.audio.IAudioStream;
import org.plutoengine.audio.ISeekableAudioTrack;
import java.nio.ShortBuffer;
public class AudioTrack extends AudioSource
{
private static final int BUFFER_SIZE_PER_CHANNEL = 16384;
private final IAudioStream track;
private final int format;
private static final int DOUBLE_BUFFER = 2;
private final int[] buffers;
private boolean closeOnFinish;
private final ShortBuffer pcm;
public AudioTrack(IAudioStream track)
{
this.track = track;
this.format = switch (track.getChannels()) {
case 1 -> AL10.AL_FORMAT_MONO16;
case 2 -> AL10.AL_FORMAT_STEREO16;
default -> throw new UnsupportedOperationException("Unsupported number of channels: " + track.getChannels());
};
int bufferSize = track.getChannels() * BUFFER_SIZE_PER_CHANNEL;
this.pcm = MemoryUtil.memAllocShort(bufferSize);
this.buffers = new int[DOUBLE_BUFFER];
AL10.alGenBuffers(this.buffers);
AL10.alSourcei(this.source, SOFTDirectChannels.AL_DIRECT_CHANNELS_SOFT, AL10.AL_TRUE);
}
@Override
public void close()
{
AL10.alDeleteBuffers(this.buffers);
AL10.alDeleteSources(this.source);
MemoryUtil.memFree(this.pcm);
}
public boolean play()
{
if (this.track instanceof ISeekableAudioTrack seekableAudioTrack)
seekableAudioTrack.rewind();
for (int buf : this.buffers)
{
this.stream(buf);
}
AL10.alSourcePlay(this.source);
return true;
}
public void setCloseOnFinish()
{
this.closeOnFinish = true;
}
private void stream(int buffer)
{
this.pcm.clear();
int samplesPerChannel = this.track.getSamples(this.pcm);
if (samplesPerChannel == 0)
return;
var samples = samplesPerChannel * this.track.getChannels();
this.pcm.limit(samples);
AL10.alBufferData(buffer, this.format, this.pcm, this.track.getSampleRate());
AL10.alSourceQueueBuffers(this.source, buffer);
}
public boolean update()
{
int processed = AL10.alGetSourcei(this.source, AL10.AL_BUFFERS_PROCESSED);
for (int i = 0; i < processed; i++)
{
int buffer = AL10.alSourceUnqueueBuffers(this.source);
this.stream(buffer);
}
if (AL10.alGetSourcei(this.source, AL10.AL_SOURCE_STATE) == AL10.AL_STOPPED)
{
if (this.closeOnFinish)
{
this.close();
}
return false;
}
return true;
}
}

View File

@ -0,0 +1,30 @@
package org.plutoengine.audio.al;
import org.plutoengine.audio.ISeekableTrack;
import java.nio.ShortBuffer;
public class AudioTrackSource extends AudioDoubleBufferedSource
{
private final ISeekableTrack track;
public AudioTrackSource(ISeekableTrack track)
{
super(track);
this.track = track;
}
public boolean play()
{
this.track.rewind();
return super.play();
}
@Override
protected int getSamples(ShortBuffer pcmTransferBuf)
{
return this.track.getSamples(pcmTransferBuf);
}
}

View File

@ -0,0 +1,34 @@
package org.plutoengine.audio.al;
import org.lwjgl.openal.AL11;
import java.util.Arrays;
public enum EnumDistanceModel implements IOpenALEnum
{
NONE(AL11.AL_NONE),
INVERSE_DISTANCE(AL11.AL_INVERSE_DISTANCE),
INVERSE_DISTANCE_CLAMPED(AL11.AL_INVERSE_DISTANCE_CLAMPED),
LINEAR_DISTANCE(AL11.AL_LINEAR_DISTANCE),
LINEAR_DISTANCE_CLAMPED(AL11.AL_LINEAR_DISTANCE_CLAMPED),
EXPONENT_DISTANCE(AL11.AL_EXPONENT_DISTANCE),
EXPONENT_DISTANCE_CLAMPED(AL11.AL_EXPONENT_DISTANCE_CLAMPED);
private final int alID;
EnumDistanceModel(int alID)
{
this.alID = alID;
}
public static EnumDistanceModel getByID(int id)
{
return Arrays.stream(EnumDistanceModel.values()).filter(model -> model.getALID() == id).findAny().orElse(null);
}
@Override
public int getALID()
{
return this.alID;
}
}

View File

@ -0,0 +1,6 @@
package org.plutoengine.audio.al;
public interface IOpenALEnum
{
int getALID();
}

View File

@ -0,0 +1,86 @@
package org.plutoengine.audio.al;
import org.jetbrains.annotations.NotNull;
import org.joml.Vector3fc;
import org.plutoengine.audio.RandomAccessClip;
import java.util.function.BooleanSupplier;
import java.util.function.UnaryOperator;
public class SoundEffect
{
private final @NotNull RandomAccessClip clip;
private @NotNull Vector3fc position;
private float volume;
private float pitch;
private UnaryOperator<Vector3fc> movementMapper;
private BooleanSupplier keepAliveFunction;
public SoundEffect(@NotNull RandomAccessClip soundEffect, @NotNull Vector3fc position, float volume)
{
this.clip = soundEffect;
this.position = position;
this.volume = volume;
this.pitch = 1.0f;
}
public SoundEffect position(Vector3fc position)
{
this.position = position;
return this;
}
public SoundEffect volume(float volume)
{
this.volume = volume;
return this;
}
public SoundEffect pitch(float pitch)
{
this.pitch = pitch;
return this;
}
public SoundEffect movementMapper(UnaryOperator<Vector3fc> movementMapper)
{
this.movementMapper = movementMapper;
return this;
}
public SoundEffect keepAliveFunction(BooleanSupplier keepAliveFunction)
{
this.keepAliveFunction = keepAliveFunction;
return this;
}
@NotNull RandomAccessClip getClip()
{
return this.clip;
}
@NotNull Vector3fc getPosition()
{
return this.position;
}
float getVolume()
{
return this.volume;
}
float getPitch()
{
return this.pitch;
}
UnaryOperator<Vector3fc> getMovementMapper()
{
return this.movementMapper;
}
BooleanSupplier getKeepAliveFunction()
{
return this.keepAliveFunction;
}
}

View File

@ -281,6 +281,7 @@ public abstract class PlutoApplication
this.display.swapBuffers();
audioEngine.update();
inputBus.resetStates();
this.display.pollEvents();

View File

@ -14,10 +14,13 @@ dependencies {
api("org.lwjgl", "lwjgl-glfw")
api("org.lwjgl", "lwjgl-opengl")
api("org.lwjgl", "lwjgl-stb")
runtimeOnly("org.lwjgl", "lwjgl", classifier = Versions.lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = Versions.lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = Versions.lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = Versions.lwjglNatives)
org.plutoengine.Versions.lwjglNatives.forEach {
runtimeOnly("org.lwjgl", "lwjgl", classifier = it)
runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = it)
runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = it)
runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = it)
}
api("com.code-disaster.steamworks4j", "steamworks4j", Versions.steamworks4jVersion)
api("com.code-disaster.steamworks4j", "steamworks4j-server", Versions.steamworks4jServerVersion)

View File

@ -16,7 +16,7 @@ public class Framerate
private static double animationTimer = 0;
private static double FPS = Double.NaN;
private static double fps = Double.NaN;
private static int interpolatedFPS;
@ -31,7 +31,7 @@ public class Framerate
public static double getFPS()
{
return FPS;
return fps;
}
public static int getInterpolatedFPS()
@ -55,7 +55,7 @@ public class Framerate
animationTimer += frameTimeNs / (double) TimeUnit.SECONDS.toNanos(1);
// Maintain precision in case the engine runs for many hours
animationTimer %= TimeUnit.DAYS.toMinutes(1);
FPS = TimeUnit.SECONDS.toMillis(1) / frameTime;
fps = TimeUnit.SECONDS.toMillis(1) / frameTime;
}
var nowMs = System.currentTimeMillis();
@ -77,7 +77,7 @@ public class Framerate
}
else
{
interpolatedFPS = (int) Math.round(FPS);
interpolatedFPS = (int) Math.round(fps);
}
lastDraw = now;

View File

@ -14,7 +14,9 @@ dependencies {
implementation("com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml", "2.12.3")
implementation("org.lwjgl", "lwjgl-yoga")
runtimeOnly("org.lwjgl", "lwjgl-yoga", classifier = org.plutoengine.Versions.lwjglNatives)
org.plutoengine.Versions.lwjglNatives.forEach {
runtimeOnly("org.lwjgl", "lwjgl-yoga", classifier = it)
}
implementation("org.commonmark", "commonmark", "0.18.1")
}

View File

@ -1,6 +1,7 @@
package org.plutoengine.graphics;
import org.plutoengine.Pluto;
import org.plutoengine.graphics.gui.BitmapFontShader;
import org.plutoengine.graphics.gui.FontShader;
import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter;
@ -28,7 +29,7 @@ public class PlutoGUIMod implements IModEntryPoint
public static FontShader fontShader;
public static FontShader bitmapFontShader;
public static BitmapFontShader bitmapFontShader;
public void onLoad(Mod mod)
{
@ -36,7 +37,7 @@ public class PlutoGUIMod implements IModEntryPoint
fontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexFontShader#glsl"), mod.getResource("shaders.FragmentFontShader#glsl")).build(FontShader.class, false);
bitmapFontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexBitmapFontShader#glsl"), mod.getResource("shaders.FragmentBitmapFontShader#glsl")).build(FontShader.class, false);
bitmapFontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexBitmapFontShader#glsl"), mod.getResource("shaders.FragmentBitmapFontShader#glsl")).build(BitmapFontShader.class, false);
uiElementsAtlas = new RectangleTexture();
uiElementsAtlas.load(mod.getResource("gui.elements#png"), MagFilter.NEAREST, MinFilter.NEAREST, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE);

View File

@ -13,7 +13,7 @@ import org.plutoengine.shader.uniform.auto.AutoViewportProjection;
import org.plutoengine.util.color.IRGBA;
@ShaderProgram
public final class BitmapTextShader extends ShaderBase implements IGUIShader
public final class BitmapFontShader extends ShaderBase implements IGUIShader
{
@AutoViewportProjection
@Uniform(name = "projection")

View File

@ -38,7 +38,10 @@ dependencies {
implementation("org.lwjgl:lwjgl")
implementation("org.lwjgl:lwjgl-xxhash")
implementation("org.lwjgl:lwjgl-zstd")
runtimeOnly("org.lwjgl", "lwjgl", classifier = Versions.lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-xxhash", classifier = Versions.lwjglNatives)
runtimeOnly("org.lwjgl", "lwjgl-zstd", classifier = Versions.lwjglNatives)
Versions.lwjglNatives.forEach {
runtimeOnly("org.lwjgl", "lwjgl", classifier = it)
runtimeOnly("org.lwjgl", "lwjgl-xxhash", classifier = it)
runtimeOnly("org.lwjgl", "lwjgl-zstd", classifier = it)
}
}

View File

@ -0,0 +1,38 @@
plugins {
id("edu.sc.seis.launch4j") version "2.5.3"
}
application {
mainClass.set("cz.tefek.srclone.Main")
}
launch4j {
mainClassName = "cz.tefek.srclone.Main"
bundledJrePath = "jre"
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<JavaExec> {
jvmArgs = listOf(
"-Dcz.tefek.pluto.debug=true",
"-Dorg.lwjgl.util.Debug=true"
)
}
distributions {
main {
contents {
from("mods") {
into("mods")
}
}
}
}
dependencies {
implementation(project(":plutoengine:plutocore"))
}

View File

@ -0,0 +1,8 @@
{
"displayName": "GLFW",
"author": "The GLFW team",
"description": "The GLFW library, used for native window creation.",
"resourceRoots": {
}
}

View File

@ -0,0 +1,8 @@
{
"displayName": "LWJGL",
"description": "Lightweight Java Game Library",
"author": "The LWJGL team",
"resourceRoots": {
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,75 @@
#version 330 core
in vec2 uvCoordinates;
in vec2 paintUVCoordinates;
uniform sampler2DRect textureSampler;
uniform int paintType;
uniform vec4 paintColor;
uniform int paintGradientStopCount;
uniform vec4[16] paintGradientColors;
uniform float[16] paintGradientPositions;
uniform vec2[2] paintGradientEnds;
out vec4 out_Color;
vec4 solidColor(void)
{
return paintColor;
}
vec4 gammaMix(vec4 lCol, vec4 rCol, float ratio)
{
float gamma = 2.2;
float one_over_gamma = 1 / gamma;
vec4 ilCol = vec4(pow(lCol.r, gamma), pow(lCol.g, gamma), pow(lCol.b, gamma), lCol.a);
vec4 irCol = vec4(pow(rCol.r, gamma), pow(rCol.g, gamma), pow(rCol.b, gamma), rCol.a);
vec4 fCol = mix(ilCol, irCol, ratio);
return vec4(pow(fCol.r, one_over_gamma), pow(fCol.g, one_over_gamma), pow(fCol.b, one_over_gamma), fCol.a);
}
vec4 gradientColor(void)
{
float angle = atan(-paintGradientEnds[1].y + paintGradientEnds[0].y, paintGradientEnds[1].x - paintGradientEnds[0].x);
float rotatedStartX = paintGradientEnds[0].x * cos(angle) - paintGradientEnds[0].y * sin(angle);
float rotatedEndX = paintGradientEnds[1].x * cos(angle) - paintGradientEnds[1].y * sin(angle);
float d = rotatedEndX - rotatedStartX;
float pX = paintUVCoordinates.x * cos(angle) - paintUVCoordinates.y * sin(angle);
float mr = smoothstep(rotatedStartX + paintGradientPositions[0] * d, rotatedStartX + paintGradientPositions[1] * d, pX);
vec4 col = gammaMix(paintGradientColors[0], paintGradientColors[1], mr);
for (int i = 1; i < paintGradientStopCount - 1; i++)
{
mr = smoothstep(rotatedStartX + paintGradientPositions[i] * d, rotatedStartX + paintGradientPositions[i + 1] * d, pX);
col = gammaMix(col, paintGradientColors[i + 1], mr);
}
return col;
}
void main(void)
{
vec4 col;
switch (paintType)
{
case 0:
col = solidColor();
break;
case 1:
col = gradientColor();
break;
}
col.rgba *= texture(textureSampler, uvCoordinates);
out_Color = col;
}

View File

@ -0,0 +1,84 @@
#version 330 core
in vec2 uvCoordinates;
flat in int atlasPage;
in vec2 paintUVCoordinates;
uniform sampler2DArray textureSampler;
uniform int paintType;
uniform vec4 paintColor;
uniform int paintGradientStopCount;
uniform vec4[16] paintGradientColors;
uniform float[16] paintGradientPositions;
uniform vec2[2] paintGradientEnds;
uniform float pxScale;
out vec4 out_Color;
vec4 solidColor(void)
{
return paintColor;
}
vec4 gammaMix(vec4 lCol, vec4 rCol, float ratio)
{
float gamma = 2.2;
float one_over_gamma = 1 / gamma;
vec4 ilCol = vec4(pow(lCol.r, gamma), pow(lCol.g, gamma), pow(lCol.b, gamma), lCol.a);
vec4 irCol = vec4(pow(rCol.r, gamma), pow(rCol.g, gamma), pow(rCol.b, gamma), rCol.a);
vec4 fCol = mix(ilCol, irCol, ratio);
return vec4(pow(fCol.r, one_over_gamma), pow(fCol.g, one_over_gamma), pow(fCol.b, one_over_gamma), fCol.a);
}
vec4 gradientColor(void)
{
float angle = atan(-paintGradientEnds[1].y + paintGradientEnds[0].y, paintGradientEnds[1].x - paintGradientEnds[0].x);
float rotatedStartX = paintGradientEnds[0].x * cos(angle) - paintGradientEnds[0].y * sin(angle);
float rotatedEndX = paintGradientEnds[1].x * cos(angle) - paintGradientEnds[1].y * sin(angle);
float d = rotatedEndX - rotatedStartX;
float pX = paintUVCoordinates.x * cos(angle) - paintUVCoordinates.y * sin(angle);
float mr = smoothstep(rotatedStartX + paintGradientPositions[0] * d, rotatedStartX + paintGradientPositions[1] * d, pX);
vec4 col = gammaMix(paintGradientColors[0], paintGradientColors[1], mr);
for (int i = 1; i < paintGradientStopCount - 1; i++)
{
mr = smoothstep(rotatedStartX + paintGradientPositions[i] * d, rotatedStartX + paintGradientPositions[i + 1] * d, pX);
col = gammaMix(col, paintGradientColors[i + 1], mr);
}
return col;
}
void main(void)
{
vec3 texCoords = vec3(uvCoordinates, atlasPage);
float threshold = 180.0 / 255.0 - 5.0 / pow(pxScale, 1.6); // Also help small text be readable
float signedDist = texture(textureSampler, texCoords).r - threshold;
vec4 col;
switch (paintType)
{
case 0:
col = solidColor();
break;
case 1:
col = gradientColor();
break;
}
col.a *= smoothstep(0, 2.4 / pxScale, signedDist);
out_Color = col;
}

View File

@ -0,0 +1,19 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
in vec2 paintUVCoords;
out vec2 uvCoordinates;
out vec2 paintUVCoordinates;
uniform mat4 projection;
uniform mat3 transformation;
void main(void)
{
uvCoordinates = uvCoords;
paintUVCoordinates = paintUVCoords;
vec3 transformed = vec3((transformation * vec3(position, 1.0)).xy, 0.0);
gl_Position = projection * vec4(transformed, 1.0);
}

View File

@ -0,0 +1,22 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
in int page;
in vec2 paintUVCoords;
out vec2 uvCoordinates;
out vec2 paintUVCoordinates;
flat out int atlasPage;
uniform mat4 projection;
uniform mat3 transformation;
void main(void)
{
atlasPage = page;
uvCoordinates = uvCoords;
paintUVCoordinates = paintUVCoords;
vec3 transformed = vec3((transformation * vec3(position, 1.0)).xy, 0.0);
gl_Position = projection * vec4(transformed, 1.0);
}

View File

@ -0,0 +1,11 @@
{
"displayName": "Pluto Engine GUI Renderer",
"description": "",
"author": "Tefek",
"resourceRoots": {
"default": {
"path": "default",
"type": "open"
}
}
}

View File

@ -0,0 +1,7 @@
{
"displayName": "Pluto Shader",
"description": "PlutoEngine's shader manager.",
"author": "Tefek",
"resourceRoots": {
}
}

View File

@ -0,0 +1,10 @@
#version 330 core
uniform vec4 color;
out vec4 out_Color;
void main(void)
{
out_Color = color;
}

View File

@ -0,0 +1,16 @@
#version 330 core
in vec2 uvCoordinates;
uniform sampler2DRect textureSampler;
uniform vec4 recolor;
out vec4 out_Color;
void main(void)
{
vec4 color = texture(textureSampler, uvCoordinates) * recolor;
out_Color = color;
}

View File

@ -0,0 +1,16 @@
#version 330 core
in vec2 uvCoordinates;
uniform sampler2DRect textureSampler;
uniform vec4 recolor;
out vec4 out_Color;
void main(void)
{
vec4 color = texture(textureSampler, uvCoordinates) * recolor;
out_Color = color;
}

View File

@ -0,0 +1,16 @@
#version 330 core
in vec2 uvCoordinates;
uniform sampler2DRect textureSampler;
uniform vec4 recolor;
out vec4 out_Color;
void main(void)
{
vec4 color = texture(textureSampler, uvCoordinates) * recolor;
out_Color = color;
}

View File

@ -0,0 +1,11 @@
#version 330 core
in vec2 position;
uniform mat4 projection;
uniform mat3x2 transformation;
void main(void)
{
gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0);
}

View File

@ -0,0 +1,19 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
out vec2 uvCoordinates;
uniform mat4 projection;
uniform mat3x2 transformation;
uniform vec2 uvBase;
uniform vec2 uvDelta;
void main(void)
{
gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0);
uvCoordinates = uvBase + uvCoords * uvDelta;
}

View File

@ -0,0 +1,19 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
out vec2 uvCoordinates;
uniform mat4 projection;
uniform mat3x2 transformation;
uniform vec2 uvBase;
uniform vec2 uvDelta;
void main(void)
{
gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0);
uvCoordinates = uvBase + uvCoords * uvDelta;
}

View File

@ -0,0 +1,19 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
out vec2 uvCoordinates;
uniform mat4 projection;
uniform mat3x2 transformation;
uniform vec2 uvBase;
uniform vec2 uvDelta;
void main(void)
{
gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0);
uvCoordinates = uvBase + uvCoords * uvDelta;
}

View File

@ -0,0 +1,16 @@
#version 330 core
in vec2 uvCoordinates;
uniform sampler2D textureSampler;
uniform vec4 recolor;
out vec4 out_Color;
void main(void)
{
vec4 color = texture(textureSampler, uvCoordinates) * recolor;
out_Color = color;
}

View File

@ -0,0 +1,19 @@
#version 330 core
in vec2 position;
in vec2 uvCoords;
out vec2 uvCoordinates;
uniform mat4 projection;
uniform mat3x2 transformation;
uniform vec2 uvBase;
uniform vec2 uvDelta;
void main(void)
{
gl_Position = projection * vec4(transformation * vec3(position.x, position.y, 1.0), 0.0, 1.0);
uvCoordinates = uvBase + uvCoords * uvDelta;
}

View File

@ -0,0 +1,11 @@
{
"displayName": "Pluto SpriteSheet",
"description": "A library to manage, store and draw sprites.",
"author": "Tefek",
"resourceRoots": {
"default": {
"path": "default",
"type": "open"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,19 @@
{
"displayName": "SRClone",
"description": "Java reimplementation of SRClone.",
"author": "Tefek",
"resourceRoots": {
"default": {
"path": "default",
"type": "open"
},
"icons": {
"path": "icons",
"type": "open"
},
"plutofonts": {
"path": "plutofonts",
"type": "open"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,249 @@
name: SRClone
meta:
scale: 8
ascent: 8
descent: 0
lineGap: 1
atlas: "pluto+asl://!font#png"
filtering: NEAREST
kern:
-
left: 0
right: 0
offset: 0
glyphs:
-
cp: 48 # 0
sprite:
x: 0
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 49 # 1
sprite:
x: 8
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 50 # 2
sprite:
x: 16
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 51 # 3
sprite:
x: 24
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 52 # 4
sprite:
x: 32
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 53 # 5
sprite:
x: 40
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 54 # 6
sprite:
x: 48
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 55 # 7
sprite:
x: 56
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 56 # 8
sprite:
x: 64
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 57 # 9
sprite:
x: 72
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 57 # 9
sprite:
x: 72
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 43 # +
sprite:
x: 80
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 45 # +
sprite:
x: 88
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 46 # .
sprite:
x: 96
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 120 # x
sprite:
x: 104
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 128150 # 💖 - sparkling heart because the default red one is two codepoints :(
sprite:
x: 112
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 105 # i
sprite:
x: 120
y: 0
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 83 # S
sprite:
x: 0
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 127776 # 🌠 - shooting star
sprite:
x: 8
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 128994 # 🟢 - large green circle
sprite:
x: 16
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 9728 # ☀ - sun
sprite:
x: 24
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 128314 # 🔺 - red up triangle
sprite:
x: 32
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 128160 # 💠 - diamond
sprite:
x: 40
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 121171 # 𝥓 - spiral arrow thingy
sprite:
x: 48
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 8734 # ∞ - infinity
sprite:
x: 112
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8
-
cp: 32 # space
sprite:
x: 120
y: 8
w: 8
h: 8
leftBearing: 0
advance: 8

View File

@ -0,0 +1,7 @@
package cz.tefek.srclone;
public enum EnumTeam
{
PLAYER,
ENEMY
}

View File

@ -0,0 +1,252 @@
package cz.tefek.srclone;
import org.lwjgl.glfw.GLFW;
import org.plutoengine.Pluto;
import org.plutoengine.PlutoLocal;
import org.plutoengine.display.Display;
import org.plutoengine.graphics.ImmediateFontRenderer;
import org.plutoengine.libra.paint.LiPaint;
import org.plutoengine.libra.text.shaping.TextStyleOptions;
import org.plutoengine.logger.Logger;
import org.plutoengine.util.color.Color;
import java.util.*;
import cz.tefek.srclone.ammo.EnumAmmo;
import cz.tefek.srclone.audio.SRAudioEngine;
import cz.tefek.srclone.entity.Entity;
import cz.tefek.srclone.entity.EntityPlayer;
import cz.tefek.srclone.entity.projectile.EntityProjectile;
import cz.tefek.srclone.particle.Particle;
public class Game
{
private float camX;
private float camY;
private float viewX;
private float viewY;
private float deathScreenAnimation;
private final List<Entity> entities;
private final List<EntityProjectile> projectiles;
private final List<Particle> particles;
private final EntityPlayer entityPlayer;
private final List<Entity> newEntities;
private final List<EntityProjectile> newProjectiles;
private final List<Particle> newParticles;
private final Random random;
private final SRAudioEngine audioEngine;
private final GameDirector director;
public Game()
{
Logger.log("==== NEW GAME ====");
this.camX = 0;
this.camY = 0;
this.entities = new ArrayList<>();
this.newEntities = new ArrayList<>();
this.projectiles = new ArrayList<>();
this.newProjectiles = new ArrayList<>();
this.entityPlayer = new EntityPlayer();
this.entityPlayer.init(this, 0.0f, 0.0f);
this.entityPlayer.addAmmo(EnumAmmo.P_LASER_BEAM, EnumAmmo.AMMO_INFINITE);
// DEBUG
if (Pluto.DEBUG_MODE)
{
Arrays.stream(EnumAmmo.values()).forEach(a -> this.entityPlayer.addAmmo(a, EnumAmmo.AMMO_INFINITE));
}
this.particles = new ArrayList<>();
this.newParticles = new ArrayList<>();
this.random = new Random();
this.audioEngine = new SRAudioEngine();
this.director = new GameDirector(this);
}
public void tick(double frameTime)
{
if (frameTime != 0 && !Double.isNaN(frameTime))
{
var ft = (float) frameTime;
if (!this.isOver())
{
this.entities.forEach(e -> e.tick(ft));
this.projectiles.forEach(p -> p.tick(ft));
this.entityPlayer.tick(ft);
this.director.tick(frameTime);
this.entities.removeIf(Entity::isDead);
this.entities.addAll(this.newEntities);
this.newEntities.clear();
this.projectiles.removeIf(EntityProjectile::isDead);
this.projectiles.addAll(this.newProjectiles);
this.newProjectiles.clear();
}
this.particles.forEach(p -> p.tick(ft));
this.particles.removeIf(Particle::isDead);
this.particles.addAll(this.newParticles);
this.newParticles.clear();
}
if (this.isOver())
{
this.deathScreenAnimation += frameTime;
this.audioEngine.stopMusic(this);
}
this.audioEngine.tick(this);
this.render();
}
private void render()
{
this.camX = -this.entityPlayer.getX();
this.camY = -this.entityPlayer.getY();
var display = PlutoLocal.components().getComponent(Display.class);
this.viewX = this.camX + display.getWidth() / 2.0f;
this.viewY = this.camY + display.getHeight() / 2.0f;
SRCloneMod.starField.render(this.viewX, this.viewY);
this.entities.forEach(Entity::render);
if (!this.isOver())
this.entityPlayer.render();
this.projectiles.forEach(Entity::render);
this.particles.forEach(Particle::render);
var selectedAmmo = this.entityPlayer.getSelectedAmmo();
long ammoCnt = this.entityPlayer.getAmmo(selectedAmmo);
String ammoStr;
if (ammoCnt == EnumAmmo.AMMO_INFINITE)
ammoStr = "%sx∞";
else
ammoStr = "%%sx%06d".formatted(ammoCnt);
var ammoFont = new TextStyleOptions(24)
.setVerticalAlign(TextStyleOptions.TextAlign.START)
.setPaint(LiPaint.solidColor(Color.WHITE));
ImmediateFontRenderer.drawString(5.0f, display.getHeight() - 58.0f, ammoStr.formatted(selectedAmmo.getTextIcon()), SRCloneMod.srCloneFont, ammoFont);
var healthFont = new TextStyleOptions(48)
.setVerticalAlign(TextStyleOptions.TextAlign.START)
.setPaint(LiPaint.solidColor(Color.WHITE));
ImmediateFontRenderer.drawString(5.0f, display.getHeight() - 5.0f, "\uD83D\uDC96%03.0f".formatted(this.entityPlayer.getHealth()), SRCloneMod.srCloneFont, healthFont);
var scoreFont = new TextStyleOptions(24)
.setPaint(LiPaint.solidColor(Color.WHITE));
ImmediateFontRenderer.drawString(5.0f, 5.0f, ("S %010.0f").formatted(this.entityPlayer.getScore()), SRCloneMod.srCloneFont, scoreFont);
if (this.deathScreenAnimation > 1.0f)
{
var youDiedStyle = new TextStyleOptions(64)
.setHorizontalAlign(TextStyleOptions.TextAlign.CENTER)
.setVerticalAlign(TextStyleOptions.TextAlign.START)
.setPaint(LiPaint.solidColor(Color.CRIMSON));
ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f - 6.0f, "You Died!", SRCloneMod.font, youDiedStyle);
youDiedStyle.setPaint(LiPaint.solidColor(Color.WHITE));
ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f - 8.0f, "You Died!", SRCloneMod.font, youDiedStyle);
var playAgainKey = String.valueOf(GLFW.glfwGetKeyName(GLFW.GLFW_KEY_R, GLFW.glfwGetKeyScancode(GLFW.GLFW_KEY_R)));
var playAgainStr = "Press [%s] to play again...".formatted(playAgainKey.toUpperCase(Locale.ROOT));
var playAgainStyle = new TextStyleOptions(40)
.setHorizontalAlign(TextStyleOptions.TextAlign.CENTER)
.setVerticalAlign(TextStyleOptions.TextAlign.END)
.setPaint(LiPaint.solidColor(Color.CRIMSON));
ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f + 20.0f, playAgainStr, SRCloneMod.font, playAgainStyle);
playAgainStyle.setPaint(LiPaint.solidColor(Color.WHITE));
ImmediateFontRenderer.drawString(display.getWidth() / 2.0f, display.getHeight() / 2.0f + 18.0f, playAgainStr, SRCloneMod.font, playAgainStyle);
}
}
public SRAudioEngine getAudioEngine()
{
return this.audioEngine;
}
public void addEntity(Entity entity, float x, float y)
{
if (entity instanceof EntityProjectile projectile)
{
this.addProjectile(projectile, x, y);
return;
}
this.newEntities.add(entity);
entity.init(this, x, y);
}
public void addParticle(Particle particle, float x, float y)
{
this.newParticles.add(particle);
particle.init(this, x, y);
}
private void addProjectile(EntityProjectile projectile, float x, float y)
{
this.newProjectiles.add(projectile);
projectile.init(this, x, y);
}
public boolean isOver()
{
return this.entityPlayer.isDead();
}
public float getDeathScreenAnimation()
{
return this.deathScreenAnimation;
}
public List<Entity> getEntities()
{
return this.entities;
}
public EntityPlayer getEntityPlayer()
{
return this.entityPlayer;
}
public Random getRandom()
{
return this.random;
}
public float getViewX()
{
return this.viewX;
}
public float getViewY()
{
return this.viewY;
}
}

View File

@ -0,0 +1,105 @@
package cz.tefek.srclone;
import org.plutoengine.PlutoLocal;
import org.plutoengine.display.Display;
import org.plutoengine.logger.Logger;
import java.util.Random;
import cz.tefek.srclone.entity.EntityPlayer;
import cz.tefek.srclone.entity.enemy.EntityEnemy;
import cz.tefek.srclone.entity.enemy.EntityEnemyScout;
import cz.tefek.srclone.entity.enemy.EntityEnemySmallBomber;
public class GameDirector
{
private double gameTime;
private double spawnTimer;
private final Game game;
private double difficulty;
GameDirector(Game game)
{
this.game = game;
}
public void tick(double delta)
{
var rand = this.game.getRandom();
this.spawnTimer -= delta;
if (this.spawnTimer <= 0)
{
this.difficulty = 10 + Math.pow(this.gameTime, 1.5f);
double spawnTimerNext = 160 / (4 + Math.log10(difficulty));
double variation = 0.75f + rand.nextDouble() / 2.0;
double rest = (1.0f + Math.sin(difficulty / 120.0) / 4.0);
double difficultyModifier = Math.sqrt(difficulty / 25.0f);
int waveSize = (int) (1 + difficultyModifier * variation * rest);
Logger.logf("Wave size: %d%n", waveSize);
Logger.logf("Next wave in: %.0f seconds%n", spawnTimerNext);
Logger.log("---------------");
var player = this.game.getEntityPlayer();
this.spawn(player, rand, waveSize);
this.spawnTimer += spawnTimerNext;
}
this.gameTime += delta;
}
public double getDifficulty()
{
return this.difficulty;
}
private void spawn(EntityPlayer player, Random rand, int budget)
{
var display = PlutoLocal.components().getComponent(Display.class);
double minSpawnRadius = 200 + Math.hypot(display.getWidth(), display.getHeight()); // HACK: Spawn enemies just outside the visible radius
float px = player.getX();
float py = player.getY();
double tdir = rand.nextDouble() * 2 * Math.PI;
for (int i = 0; i < budget; i++)
{
EntityEnemy enemy;
int enemyTypes = 2;
switch (rand.nextInt(enemyTypes))
{
case 1:
if (budget >= 5)
{
enemy = new EntityEnemySmallBomber();
i += 2;
break;
}
case 0:
default:
enemy = new EntityEnemyScout();
}
double varDir = (rand.nextDouble() - 0.5) * (Math.PI / 8);
double t = tdir + varDir;
double x = px + Math.cos(t) * minSpawnRadius;
double y = py + Math.sin(t) * minSpawnRadius;
this.game.addEntity(enemy, (float) x, (float) y);
}
}
}

View File

@ -0,0 +1,6 @@
package cz.tefek.srclone;
public interface IGameObject
{
void init(Game game, float x, float y);
}

View File

@ -0,0 +1,68 @@
package cz.tefek.srclone;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL33;
import org.plutoengine.PlutoApplication;
import org.plutoengine.PlutoLocal;
import org.plutoengine.display.Framerate;
import org.plutoengine.graphics.ImmediateFontRenderer;
import org.plutoengine.input.Keyboard;
import org.plutoengine.libra.paint.LiPaint;
import org.plutoengine.libra.text.shaping.TextStyleOptions;
import org.plutoengine.math.ProjectionMatrix;
import org.plutoengine.shader.uniform.auto.AutomaticUniforms;
import org.plutoengine.util.color.Color;
import java.util.concurrent.TimeUnit;
public class Main extends PlutoApplication
{
public static void main(String[] args)
{
var app = new Main();
var cfg = new PlutoApplication.StartupConfig();
cfg.windowInitialDimensions(1280, 720);
cfg.windowName("jsr-clone");
// cfg.vsync(1);
app.run(args, cfg);
}
private Game game;
@Override
protected Class<?> getMainModule()
{
return SRCloneMod.class;
}
@Override
protected void init()
{
this.game = new Game();
}
@Override
protected void loop()
{
GL33.glEnable(GL33.GL_BLEND);
GL33.glBlendFunc(GL33.GL_SRC_ALPHA, GL33.GL_ONE_MINUS_SRC_ALPHA);
GL33.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL33.glClear(GL33.GL_COLOR_BUFFER_BIT);
var projection = ProjectionMatrix.createOrtho2D(this.display.getWidth(), this.display.getHeight());
AutomaticUniforms.VIEWPORT_PROJECTION.fire(projection);
var keyboard = PlutoLocal.components().getComponent(Keyboard.class);
if (this.game.isOver() && keyboard.pressed(GLFW.GLFW_KEY_R))
{
this.game = new Game();
}
var delta = Framerate.getFrameTime() / TimeUnit.SECONDS.toMillis(1);
this.game.tick(delta);
ImmediateFontRenderer.drawString(this.display.getWidth(), 5, Framerate.getInterpolatedFPS() + " FPS", SRCloneMod.font, new TextStyleOptions(32).setPaint(LiPaint.solidColor(Color.WHITE)).setHorizontalAlign(TextStyleOptions.TextAlign.END));
}
}

View File

@ -0,0 +1,148 @@
package cz.tefek.srclone;
import org.plutoengine.audio.AudioLoader;
import org.plutoengine.audio.RandomAccessClip;
import org.plutoengine.audio.SeekableTrack;
import org.plutoengine.graphics.PlutoGUIMod;
import org.plutoengine.graphics.gl.vao.QuadPresets;
import org.plutoengine.graphics.gl.vao.VertexArray;
import org.plutoengine.graphics.gui.font.bitmap.BitmapFont;
import org.plutoengine.graphics.gui.font.stbttf.STBTTFont;
import org.plutoengine.graphics.texture.texture2d.RectangleTexture;
import org.plutoengine.libra.text.font.LiFontFamily;
import org.plutoengine.libra.text.shaping.TextStyleOptions;
import org.plutoengine.mod.IModEntryPoint;
import org.plutoengine.mod.Mod;
import org.plutoengine.mod.ModEntry;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import cz.tefek.srclone.graphics.DirectionalSprite;
import cz.tefek.srclone.graphics.StarField;
@ModEntry(modID = "tefek.srclone", version = "0.1", dependencies = PlutoGUIMod.class)
public class SRCloneMod implements IModEntryPoint
{
public static LiFontFamily<STBTTFont> font;
public static LiFontFamily<BitmapFont> srCloneFont;
public static SeekableTrack playingMusic;
public static RandomAccessClip[] explosionSound;
public static RandomAccessClip impactSound;
public static Map<String, RandomAccessClip> shootSounds;
public static VertexArray centeredQuad;
public static RectangleTexture[] stars;
public static StarField starField;
public static RectangleTexture projectilesBase;
public static DirectionalSprite player;
public static RectangleTexture rocketNozzle;
public static DirectionalSprite enemyScout;
public static DirectionalSprite enemySmallBomber;
public static DirectionalSprite pickupBox;
public static DirectionalSprite parExplosion;
public static DirectionalSprite parImpact;
@Override
public void onLoad(Mod mod)
{
font = new LiFontFamily<>();
font.add(TextStyleOptions.STYLE_REGULAR, STBTTFont.load(mod.getResource("plutofonts$plutostardust#ttf")));
srCloneFont = new LiFontFamily<>();
srCloneFont.add(TextStyleOptions.STYLE_REGULAR, BitmapFont.load(mod.getResource("plutofonts$srclone.info#yaml")));
playingMusic = AudioLoader.loadMemoryDecoded(mod.getResource("sound.game_st#ogg"));
explosionSound = IntStream.range(0, 5)
.mapToObj("sound.explosion%d#ogg"::formatted)
.map(mod::getResource)
.map(AudioLoader::loadMemoryPCM)
.toArray(RandomAccessClip[]::new);
impactSound = AudioLoader.loadMemoryPCM(mod.getResource("sound.hit#ogg"));
shootSounds = Stream.of("laser", "heatStar", "polySwarm", "electronFlare", "plasma", "tachyon")
.collect(Collectors.toMap(Function.identity(), name -> {
var path = "sound.shoot.%s#ogg".formatted(name);
var resource = mod.getResource(path);
assert resource != null;
var audio = AudioLoader.loadMemoryPCM(resource);
assert audio != null;
return audio;
}));
centeredQuad = QuadPresets.halvedSize();
stars = Stream.of("blue", "red", "redDwarf", "whiteDwarf", "blue").map(starName -> {
var tex = new RectangleTexture();
tex.load(mod.getResource("textures.stars.%s#png".formatted(starName)));
return tex;
}).toList().toArray(RectangleTexture[]::new);
starField = new StarField(0, 0, 8192, 8192, stars);
projectilesBase = new RectangleTexture();
projectilesBase.load(mod.getResource("textures.particles.projectilesBase#png"));
player = DirectionalSprite.create(mod.getResource("textures.entities.ship#png"), 32, 64, 64, 32);
rocketNozzle = new RectangleTexture();
rocketNozzle.load(mod.getResource("textures.particles.rocketNozzle#png"));
enemyScout = DirectionalSprite.create(mod.getResource("textures.entities.e_scout#png"), 16, 128, 128, 16);
enemySmallBomber = DirectionalSprite.create(mod.getResource("textures.entities.e_small_bomber#png"), 16, 128, 128, 16);
parExplosion = DirectionalSprite.create(mod.getResource("textures.particles.explosion1#png"), 16, 128, 128, 16);
parImpact = DirectionalSprite.create(mod.getResource("textures.particles.impact1#png"), 10, 64, 64, 10);
pickupBox = DirectionalSprite.create(mod.getResource("textures.entities.box#png"), 16, 64, 64, 16);
}
@Override
public void onUnload()
{
pickupBox.close();
parImpact.close();
parExplosion.close();
enemySmallBomber.close();
enemyScout.close();
rocketNozzle.close();
player.close();
projectilesBase.close();
for (var star : stars)
star.close();
centeredQuad.close();
shootSounds.values()
.forEach(RandomAccessClip::close);
impactSound.close();
for (var track : explosionSound)
track.close();
playingMusic.close();
srCloneFont.close();
font.close();
}
}

View File

@ -0,0 +1,89 @@
package cz.tefek.srclone.ammo;
import org.plutoengine.audio.RandomAccessClip;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import cz.tefek.srclone.EnumTeam;
import cz.tefek.srclone.SRCloneMod;
import cz.tefek.srclone.entity.projectile.*;
public enum EnumAmmo
{
P_LASER_BEAM("", "laser", 0.2f, EnumTeam.PLAYER, EntityProjectilePlayerLaserBeam::new),
P_HEAT_STAR("\uD83C\uDF20", "heatStar", 0.4f, EnumTeam.PLAYER, EntityProjectilePlayerHeatStar::new),
P_ELECTRON_FLARE("\uD83D\uDCA0", "electronFlare", 2.5f, EnumTeam.PLAYER, EntityProjectilePlayerElectronFlare::new),
P_POLY_SWARM("\uD83D\uDD3A", "polySwarm", 0.1f, EnumTeam.PLAYER, EntityProjectilePlayerPolySwarm::new),
P_PLASMA_DISC("\uD83D\uDFE2", "plasma", 2.5f, EnumTeam.PLAYER, EntityProjectilePlayerPlasmaDisc::new),
P_TACHYON_DISC("\uD836\uDD53", "tachyon", 2.0f, EnumTeam.PLAYER, EntityProjectilePlayerTachyonDisc::new),
E_LASER_BEAM("", "laser", 2.0f, EnumTeam.ENEMY, EntityProjectileEnemyLaserBeam::new),
E_HEAT_STAR("\uD83C\uDF20", "heatStar", 3.0f, EnumTeam.ENEMY, EntityProjectileEnemyHeatStar::new);
private final EnumTeam team;
private final String shootSound;
private final float cooldown;
private final String textIcon;
private final Supplier<EntityProjectileAmmo> projectileSupplier;
EnumAmmo(String textIcon, String shootSound, float cooldown, EnumTeam team, Supplier<EntityProjectileAmmo> projectileCreateFunc)
{
this.textIcon = textIcon;
this.shootSound = shootSound;
this.cooldown = cooldown;
this.team = team;
this.projectileSupplier = projectileCreateFunc;
}
private static final List<EnumAmmo> PLAYER_SELECTABLE = Arrays.stream(EnumAmmo.values())
.filter(EnumAmmo::isPlayerSelectable)
.toList();
public static final long AMMO_INFINITE = Long.MAX_VALUE;
public static EnumAmmo next(EnumAmmo ammo)
{
int idx = PLAYER_SELECTABLE.indexOf(ammo);
return PLAYER_SELECTABLE.get((idx + 1) % PLAYER_SELECTABLE.size());
}
public static EnumAmmo previous(EnumAmmo ammo)
{
int idx = PLAYER_SELECTABLE.indexOf(ammo);
int size = PLAYER_SELECTABLE.size();
return PLAYER_SELECTABLE.get((idx - 1 + size) % size);
}
public float getCooldown()
{
return this.cooldown;
}
public RandomAccessClip getShootSound()
{
return SRCloneMod.shootSounds.get(this.shootSound);
}
public EntityProjectileAmmo createProjectile()
{
return this.projectileSupplier.get();
}
public boolean isPlayerSelectable()
{
return this.team == EnumTeam.PLAYER;
}
public String getTextIcon()
{
return this.textIcon;
}
}

View File

@ -0,0 +1,79 @@
package cz.tefek.srclone.audio;
import org.joml.Matrix4x3f;
import org.joml.Vector3f;
import org.plutoengine.PlutoLocal;
import org.plutoengine.audio.RandomAccessClip;
import org.plutoengine.audio.al.AudioContext;
import org.plutoengine.audio.al.AudioEngine;
import org.plutoengine.audio.al.AudioTrackSource;
import org.plutoengine.audio.al.SoundEffect;
import cz.tefek.srclone.Game;
import cz.tefek.srclone.SRCloneMod;
public class SRAudioEngine
{
private AudioTrackSource music;
private final AudioEngine audioEngine;
private final AudioContext context;
public SRAudioEngine()
{
this.audioEngine = PlutoLocal.components().getComponent(AudioEngine.class);
this.context = this.audioEngine.getContext();
this.context.setTransformation(new Matrix4x3f().scale(1 / 1000.0f, 1 / 1000.0f, 1.0f));
}
public void tick(Game game)
{
var player = game.getEntityPlayer();
this.context.setPosition(new Vector3f(player.getX(), player.getY(), -1));
if (this.music == null && !game.isOver())
{
this.music = new AudioTrackSource(SRCloneMod.playingMusic);
this.music.volume(0.1f);
this.music.play();
}
else if (this.music != null && !this.music.update())
this.music.play();
}
public void stopMusic(Game game)
{
if (this.music != null)
{
var mute = 1 - game.getDeathScreenAnimation();
if (mute < 0)
{
this.music.stop();
this.music = null;
return;
}
this.music.volume(0.2f * mute);
}
}
public AudioEngine getAudioEngine()
{
return this.audioEngine;
}
public void playSoundEffect(RandomAccessClip seekableAudioTrack, float x, float y, float volume)
{
var soundEffect = new SoundEffect(seekableAudioTrack, new Vector3f(x, y, 0), volume);
this.audioEngine.playSound(soundEffect);
}
public void playSoundEffect(RandomAccessClip seekableAudioTrack, float x, float y, float volume, float pitch)
{
var soundEffect = new SoundEffect(seekableAudioTrack, new Vector3f(x, y, 0), volume);
soundEffect.pitch(pitch);
this.audioEngine.playSound(soundEffect);
}
}

Some files were not shown because too many files have changed in this diff Show More