Updated the versioning system, README and added the audio submodule

This commit is contained in:
Tefek 2020-09-04 14:59:14 +02:00
parent 8edb79c69b
commit 5d109865f6
17 changed files with 775 additions and 27 deletions

View File

@ -1,3 +1,56 @@
# plutoengine
My hobby game engine. This repository unifies all my previous Pluto repositories.
### Versioning
All submodules share a version number for simplicity reasons.
Since version `20.2.0.0-alpha.0`, PlutoEngine uses
a combined version of [semantic versioning](https://semver.org/)
and [calendar versioning](https://calver.org/), the first number
denotes the year.
Therefore, the version format is always `YY.MAJOR.MINOR.PATCH-prerelease`.
*Only `major` and `year` version changes will bring breaking API changes,
**except for pre-release versions**, which may introduce breaking changes
at any time. Pre-release versions will never increment the `minor` or `patch`
version numbers.*
## Usability status of submodules
### Safe submodules
* **PlutoCore** - Stable
* **PlutoFrameBuffer** - Stable
* **PlutoGUI** - Stable, awaiting a rewrite
* **PlutoMesher** - Stable
* **PlutoShader** - Stable
* **PlutoSpritesheet** - Stable, some features are unfinished
* **PlutoStatic** - Stable, collision API nowhere near completition
### Unstable submodules
* **PlutoAudio** - Somewhat usable, unfinished
* **PlutoLib** - Somewhat usable, requires further polish
### Broken submodules, do NOT use
* **PlutoCommandParser** - Unfinished, broken, unusable
* **PlutoDB** - Broken, unusable
## Current priorities
### Very high priority
* Finishing PlutoAudio
* Further engine restructure
### High priority
* Finishing PlutoCommandParser
* The stage system and automated asset loading
### Normal priority
* The collision system for PlutoStatic
### Low priority
* Polishing PlutoLib
* A networking API

View File

@ -4,16 +4,35 @@ allprojects {
apply plugin: 'java'
group = "cz.tefek"
version = "20.1.0"
version = "20.2.0.0-alpha.0"
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
subprojects {
apply plugin: 'maven-publish'
project.ext.lwjglVersion = "3.2.3"
project.ext.jomlVersion = "1.9.25"
project.ext.steamworks4jVersion = "1.8.0"
project.ext.steamworks4jServerVersion = "1.8.0"
publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
}
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
@ -26,17 +45,7 @@ subprojects {
break
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
repositories {
mavenCentral()
}
}

10
plutoaudio/build.gradle Normal file
View File

@ -0,0 +1,10 @@
apply plugin: 'java-library'
description = "PlutoEngine's sound subsystem."
dependencies {
api project(":plutostatic")
api "org.lwjgl:lwjgl-openal"
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
}

View File

@ -0,0 +1,275 @@
package cz.tefek.pluto.engine.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.Files;
import cz.tefek.pluto.engine.buffer.BufferHelper;
import cz.tefek.pluto.io.asl.resource.ResourceAddress;
import cz.tefek.pluto.io.logger.Logger;
import cz.tefek.pluto.io.logger.SmartSeverity;
public class AudioLoader
{
/**
* Loads an audio track denoted by this {@link ResourceAddress} into memory
* for from-memory Vorbis decoding and streaming. Good for frequently used
* 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(ResourceAddress address)
{
Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", address.toString());
try
{
return new MemoryDecodedVorbisTrack(address);
}
catch (IOException e)
{
Logger.logf(SmartSeverity.AUDIO_ERROR, "Failed to load an audio file: '%s'\n", address.toString());
e.printStackTrace();
return null;
}
}
/**
* Loads an audio track denoted by this {@link ResourceAddress} into memory
* for from-memory PCM streaming. Good for frequently used small audio
* files.
*/
public static ISeekableAudioTrack loadMemoryPCM(ResourceAddress address)
{
Logger.logf(SmartSeverity.AUDIO_PLUS, "Loading audio file: %s\n", address.toString());
try
{
return new MemoryPCMTrack(address);
}
catch (IOException e)
{
Logger.logf(SmartSeverity.AUDIO_ERROR, "Failed to load an audio file: '%s'\n", address.toString());
e.printStackTrace();
return null;
}
}
private static ByteBuffer loadIntoMemory(ResourceAddress addr) throws IOException
{
var path = addr.toNIOPath();
var size = Files.size(path);
if (size > Integer.MAX_VALUE)
{
throw new IOException("File '" + addr.toString() + "' is too big to be loaded!");
}
var readData = MemoryUtil.memAlloc((int) size);
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 ShortBuffer pcmAudio = null;
private int sampleOffset = 0;
private MemoryPCMTrack(ResourceAddress address) throws IOException
{
long handle = MemoryUtil.NULL;
ByteBuffer audioBytes = null;
try (MemoryStack stack = MemoryStack.stackPush())
{
audioBytes = loadIntoMemory(address);
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", address.toString(), error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.mallocStack(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(ResourceAddress address) throws IOException
{
try
{
this.encodedAudio = loadIntoMemory(address);
}
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", address.toString(), error.get(0)));
}
STBVorbisInfo info = STBVorbisInfo.mallocStack(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\n\t\tChannels:\t%d\n\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,14 @@
package cz.tefek.pluto.engine.audio;
import java.nio.ShortBuffer;
public interface IAudioStream
{
int getSamples(ShortBuffer pcm);
int getSampleRate();
int getChannels();
void close();
}

View File

@ -0,0 +1,25 @@
package cz.tefek.pluto.engine.audio;
public interface ISeekableAudioTrack extends IAudioStream
{
int getSampleOffset();
int getLengthInSamples();
default void skip(int sampleCount)
{
this.seek(Math.min(Math.max(0, this.getSampleOffset() + sampleCount), this.getLengthInSamples()));
}
default void skipTo(float offset0to1)
{
this.seek(Math.round(this.getLengthInSamples() * offset0to1));
}
default void rewind()
{
this.seek(0);
}
void seek(int sampleIndex);
}

View File

@ -0,0 +1,128 @@
package cz.tefek.pluto.engine.audio.al;
import org.joml.Vector3f;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC;
import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALCCapabilities;
import org.lwjgl.openal.ALCapabilities;
import org.lwjgl.openal.EXTThreadLocalContext;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import javax.annotation.concurrent.ThreadSafe;
import cz.tefek.pluto.Pluto;
import cz.tefek.pluto.io.logger.Logger;
import cz.tefek.pluto.io.logger.SmartSeverity;
/**
* @author 493msi
*
*/
@ThreadSafe
public class AudioEngine
{
private static ThreadLocal<Long> device = new ThreadLocal<>() {
@Override
protected Long initialValue()
{
return MemoryUtil.NULL;
}
};
private static ThreadLocal<Long> context = new ThreadLocal<>() {
@Override
protected Long initialValue()
{
return MemoryUtil.NULL;
}
};
private static ThreadLocal<ALCapabilities> capabilities = new ThreadLocal<>();
public static void initialize()
{
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;
}
device.set(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;
}
context.set(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);
}
capabilities.set(alCapabilities);
}
public static void setSpeed(Vector3f speed)
{
AL10.alListener3f(AL10.AL_VELOCITY, speed.x, speed.y, speed.z);
}
public static void setPosition(Vector3f position)
{
AL10.alListener3f(AL10.AL_POSITION, position.x, position.y, position.z);
}
public static void setVolume(float volume)
{
AL10.alListenerf(AL10.AL_GAIN, volume);
}
public static 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 static boolean isReady()
{
return capabilities.get() != null;
}
public static void exit()
{
EXTThreadLocalContext.alcSetThreadContext(MemoryUtil.NULL);
ALC10.alcDestroyContext(context.get());
ALC10.alcCloseDevice(device.get());
context.remove();
device.remove();
}
}

View File

@ -0,0 +1,45 @@
package cz.tefek.pluto.engine.audio.al;
import org.joml.Vector3fc;
import org.lwjgl.openal.AL10;
public abstract class AudioSource
{
protected final int source;
protected AudioSource()
{
this.source = AL10.alGenSources();
}
public void stop()
{
AL10.alSourceStop(this.source);
}
public void close()
{
this.stop();
AL10.alDeleteSources(this.source);
}
public void position(Vector3fc pos)
{
AL10.alSource3f(this.source, AL10.AL_POSITION, pos.x(), pos.y(), pos.z());
}
public void velocity(Vector3fc velocity)
{
AL10.alSource3f(this.source, AL10.AL_VELOCITY, velocity.x(), velocity.y(), velocity.z());
}
public void pitch(float f)
{
AL10.alSourcef(this.source, AL10.AL_PITCH, f);
}
public void volume(float f)
{
AL10.alSourcef(this.source, AL10.AL_GAIN, f);
}
}

View File

@ -0,0 +1,124 @@
package cz.tefek.pluto.engine.audio.al;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.SOFTDirectChannels;
import org.lwjgl.system.MemoryUtil;
import java.nio.ShortBuffer;
import cz.tefek.pluto.engine.audio.IAudioStream;
import cz.tefek.pluto.engine.audio.ISeekableAudioTrack;
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;
switch (track.getChannels())
{
case 1:
this.format = AL10.AL_FORMAT_MONO16;
break;
case 2:
this.format = AL10.AL_FORMAT_STEREO16;
break;
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)
{
((ISeekableAudioTrack) this.track).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,25 @@
package cz.tefek.pluto.engine.audio.util;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import cz.tefek.pluto.io.logger.Logger;
import cz.tefek.pluto.io.logger.SmartSeverity;
public class AudioUtil
{
public static void printInfo(long handle, STBVorbisInfo info)
{
Logger.log(SmartSeverity.AUDIO, "stream length, samples: " + STBVorbis.stb_vorbis_stream_length_in_samples(handle));
Logger.log(SmartSeverity.AUDIO, "stream length, seconds: " + STBVorbis.stb_vorbis_stream_length_in_seconds(handle));
Logger.log(SmartSeverity.AUDIO);
Logger.log(SmartSeverity.AUDIO, "channels = " + info.channels());
Logger.log(SmartSeverity.AUDIO, "sampleRate = " + info.sample_rate());
Logger.log(SmartSeverity.AUDIO, "maxFrameSize = " + info.max_frame_size());
Logger.log(SmartSeverity.AUDIO, "setupMemoryRequired = " + info.setup_memory_required());
Logger.log(SmartSeverity.AUDIO, "setupTempMemoryRequired() = " + info.setup_temp_memory_required());
Logger.log(SmartSeverity.AUDIO, "tempMemoryRequired = " + info.temp_memory_required());
}
}

View File

@ -166,7 +166,7 @@ public class CommandParser
private boolean hasEmptyPrefix()
{
return this.prefixes.stream().filter(Predicate.not(OfInt::hasNext)).findAny().isPresent();
return this.prefixes.stream().anyMatch(Predicate.not(OfInt::hasNext));
}
/**
@ -241,7 +241,7 @@ public class CommandParser
// At this point we are 100% sure the command was resolved and can validate the parameters
/**
/*
*
* TODO: Validate parameters here
*

View File

@ -15,8 +15,8 @@ public final class CommandRegistry
{
private static CommandRegistry instance;
private Set<CommandBase> commands;
private Map<String, CommandBase> aliasTable;
private final Set<CommandBase> commands;
private final Map<String, CommandBase> aliasTable;
static
{

View File

@ -4,4 +4,5 @@ description = "The foundation module for games and apps built on top of PlutoEng
dependencies {
api project(":plutogui")
api project(":plutoaudio")
}

View File

@ -6,6 +6,7 @@ import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL33;
import cz.tefek.pluto.engine.audio.al.AudioEngine;
import cz.tefek.pluto.engine.buffer.GLFWImageUtil;
import cz.tefek.pluto.engine.display.Display;
import cz.tefek.pluto.engine.display.DisplayBuilder;
@ -17,34 +18,65 @@ import cz.tefek.pluto.modloader.ModLoaderCore;
public abstract class PlutoApplication
{
public static final boolean DEBUG_MODE = Boolean.valueOf(System.getProperty("cz.tefek.pluto.debug"));
protected Display display;
protected abstract Class<?> getMainModule();
protected abstract void loop();
public final void run(String[] args)
protected static class StartupConfig
{
public boolean coreProfile = true;
public int majorOpenGLVersion = 3;
public int minorOpenGLVersion = 3;
public String windowName = "Pluto Engine";
public int windowMSAA = 4;
public int windowInitialWidth = 1280;
public int windowInitialHeight = 720;
public int windowMinWidth = 1000;
public int windowMinHeight = 600;
public int vsync = 0;
public boolean windowResizable = true;
public StartupConfig()
{
}
}
public final void run(String[] args, StartupConfig config)
{
if (config == null)
{
config = new StartupConfig();
}
Logger.setup();
Logger.log(SmartSeverity.INFO, "Debug mode: " + (DEBUG_MODE ? "enabled" : "disabled"));
Logger.log(SmartSeverity.INFO, "Debug mode: " + (Pluto.DEBUG_MODE ? "enabled" : "disabled"));
PlutoL10n.init(Locale.UK);
DisplayBuilder.initGLFW();
this.display = new DisplayBuilder().hintOpenGLVersion(3, 3).hintDebugContext(DEBUG_MODE).hintMSAA(4).hintVisible(true).hintResizeable(true).setInitialSize(1280, 720).export();
if (config.coreProfile)
{
this.display = new DisplayBuilder().hintOpenGLVersion(config.majorOpenGLVersion, config.minorOpenGLVersion).hintDebugContext(Pluto.DEBUG_MODE).hintMSAA(config.windowMSAA).hintVisible(true).hintResizeable(config.windowResizable).setInitialSize(config.windowInitialWidth, config.windowInitialHeight).export();
}
else
{
this.display = new DisplayBuilder().hintOpenGLVersionLegacy(config.majorOpenGLVersion, config.minorOpenGLVersion).hintDebugContext(Pluto.DEBUG_MODE).hintMSAA(config.windowMSAA).hintVisible(true).hintResizeable(config.windowResizable).setInitialSize(config.windowInitialWidth, config.windowInitialHeight).export();
}
this.display.create("Stardust Miner");
this.display.create(config.windowName);
this.display.setWindowSizeLimits(1000, 600, GLFW.GLFW_DONT_CARE, GLFW.GLFW_DONT_CARE);
this.display.setWindowSizeLimits(config.windowMinWidth, config.windowMinHeight, GLFW.GLFW_DONT_CARE, GLFW.GLFW_DONT_CARE);
this.display.lockSwapInterval(0);
this.display.lockSwapInterval(config.vsync);
this.display.show();
// TODO Un-hardcode these
var icons = GLFWImageUtil.loadIconSet("data/icon16.png", "data/icon32.png", "data/icon64.png", "data/icon128.png");
this.display.setIcons(icons);
@ -53,6 +85,8 @@ public abstract class PlutoApplication
InputBus.init(this.display);
AudioEngine.initialize();
ModLoaderCore.registerMod(this.getMainModule());
ModLoaderCore.loadProcedure();
@ -72,6 +106,8 @@ public abstract class PlutoApplication
this.display.pollEvents();
}
AudioEngine.exit();
InputBus.destroy();
ModLoaderCore.unloadProcedure();

View File

@ -16,7 +16,11 @@ public enum SmartSeverity implements ISeverity
WARNING("[!] ", true),
ERROR("[X] ", true),
AUDIO("[♪] ", false),
AUDIO("[i] [♪] ", false),
AUDIO_PLUS("[+] [♪] ", false),
AUDIO_MINUS("[-] [♪] ", false),
AUDIO_WARNING("[!] [♪] ", true),
AUDIO_ERROR("[X] [♪] ", true),
MODULE("[i] [M] ", false),
MODULE_PLUS("[+] [M] ", false),

View File

@ -7,12 +7,10 @@ dependencies {
api "org.lwjgl:lwjgl"
api "org.lwjgl:lwjgl-glfw"
api "org.lwjgl:lwjgl-openal"
api "org.lwjgl:lwjgl-opengl"
api "org.lwjgl:lwjgl-stb"
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives"

View File

@ -8,6 +8,7 @@ include 'plutolib',
'plutoframebuffer',
'plutospritesheet',
'plutogui',
'plutoaudio',
'plutocore'
rootProject.name = 'plutoengine'