Introducing utility library, implementing Localizer

This commit is contained in:
Guy Perfect 2020-07-31 14:20:27 -05:00
parent 99dbc9f0cb
commit 5ee08a4d93
10 changed files with 1928 additions and 7 deletions

0
locale/.gitignore vendored
View File

6
locale/en_US.txt Normal file
View File

@ -0,0 +1,6 @@
app {
title {
empty PVB Emulator
loaded {app.filename} - {app.title.empty}
}
}

View File

@ -19,7 +19,7 @@ default:
@echo
@echo "Usage: make <recipe>"
@echo " Package recipes:"
@echo " build Produces a .jar and deletes all intermediate files"
@echo " bundle Produces a .jar and deletes all intermediate files"
@echo " clean Deletes all output files"
@echo " core Check the native core library for style errors"
@echo " desktop Compiles the Java desktop application"
@ -39,8 +39,8 @@ default:
###############################################################################
# Perform a full build process
.PHONY: build
build:
.PHONY: bundle
bundle:
@make -s core
@make -s desktop
@make -s native
@ -67,7 +67,7 @@ core:
.PHONY: desktop
desktop: clean_desktop
@echo " Compiling Java desktop application"
@javac -sourcepath src/desktop -d . src/desktop/Main.java
@javac -sourcepath src/desktop -d . -Xlint:unchecked src/desktop/Main.java
# Build all native modules
.PHONY: native
@ -83,12 +83,12 @@ pack:
$(eval jarname = "pvbemu_`date +%Y%m%d`.jar")
@echo " Bundling into $(jarname)"
@jar -cfe $(jarname) Main *.class \
locale native src vue makefile license.txt
locale native src util vue makefile license.txt
# Delete only Java .class files
.PHONY: clean_desktop
clean_desktop:
@rm -r -f *.class vue
@rm -r -f *.class util vue
# Delete everything but the .jar
.PHONY: clean_most

View File

@ -1,9 +1,18 @@
import java.util.*;
import javax.swing.*;
import util.*;
// Desktop application primary class
public class Main {
// Program entry point
public static void main(String[] args) {
System.out.println("Hello, world!");
var loc = new Localizer();
loc.set(Util.textRead("locale/en_US.txt"));
loc.put("app.filename", "wario.vb");
var window = new JFrame();
loc.add(window, "app.title.loaded");
window.setVisible(true);
}
}

View File

@ -0,0 +1,585 @@
package util;
// Java imports
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import javax.sound.sampled.*;
// Background timer using either a realtime clock or audio for timing
public class FrameTimer {
// Instance fields
private Callback callback; // Event handler
private double rate; // Frame rate in seconds
private int source; // Timing source
private int state; // Operation state
// Audio fields
private int block; // Current byte buffer index
private byte[][] blocks; // Reusable byte buffers
private double buffer; // Buffer length in seconds
private int channels; // Number channels
private double delay; // Delay in seconds
private int frames; // Audio frames per timer frame
private SourceDataLine line; // Output line
private int sampleRate; // Rate in samples per second
private boolean written; // Samples have been written this frame
private LinkedBlockingQueue<byte[]> queue; // Sample queue
// Threading fields
private Thread audio; // Background audio processing
private CyclicBarrier cycAudio; // Synchronization with audio thread
private CyclicBarrier cycTimer; // Synchronization with timer thread
private ReentrantLock lock; // Allows callback to control the timer
private Thread timer; // Background timer processing
// Type for timer event handler
public interface Callback {
void onFrame(FrameTimer source);
}
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Sources
public static final int CLOCK = 0;
public static final int AUDIO = 1;
// States
public static final int STOPPED = 0;
public static final int RUNNING = 1;
public static final int PAUSED = 2;
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public FrameTimer() {
buffer = 0.05;
cycAudio = new CyclicBarrier(2);
cycTimer = new CyclicBarrier(2);
channels = 2;
delay = 0.1;
lock = new ReentrantLock();
queue = new LinkedBlockingQueue<byte[]>();
rate = 1;
sampleRate = 44100;
state = STOPPED;
}
///////////////////////////////////////////////////////////////////////////
// Public Methods //
///////////////////////////////////////////////////////////////////////////
// Retrieve the audio buffer size
public synchronized double getBuffer() {
return buffer;
}
// Retrieve the number of audio channels
public synchronized int getChannels() {
return channels;
}
// Retrieve the audio delay
public synchronized double getDelay() {
return delay;
}
// Calculate the number of audio frames per timer frame
public synchronized int getFrames() {
return audioFrames(rate);
}
// Retrieve the frame rate
public synchronized double getRate() {
return rate;
}
// Retrieve the audio sampling rate
public synchronized int getSampleRate() {
return sampleRate;
}
// Retrieve the timing source
public synchronized int getSource() {
return source;
}
// Retrieve the operation state
public synchronized int getState() {
return state;
}
// Pause the timer
public synchronized boolean pause() {
boolean isTimer = Thread.currentThread() == timer;
// The timer thread already owns the lock
if (isTimer && lock.isHeldByCurrentThread()) {
if (state == RUNNING)
state = PAUSED;
return state == PAUSED;
}
// Obtain the lock
lock.lock();
// Invalid state
if (state != RUNNING) {
lock.unlock();
return state == PAUSED;
}
// Configure state
state = PAUSED;
// Pause timer thread
if (!isTimer) try {
timer.interrupt();
lock.unlock();
cycTimer.await();
} catch (Exception e) { }
return true;
}
// Resume running the paused timer
public synchronized boolean resume() {
boolean isTimer = Thread.currentThread() == timer;
// The timer thread already owns the lock
if (isTimer && lock.isHeldByCurrentThread()) {
if (state == PAUSED)
state = RUNNING;
return state == RUNNING;
}
// Obtain the lock
lock.lock();
// Invalid state
if (state != PAUSED) {
lock.unlock();
return state == RUNNING;
}
// Configure state
state = RUNNING;
// Resume timer thread
if (!isTimer) try {
lock.unlock();
cycTimer.await();
} catch (Exception e) { }
return true;
}
// Specify the audio buffer length
public synchronized double setBuffer(double buffer) {
return this.buffer = state == STOPPED && buffer > 0 ?
buffer : this.buffer;
}
// Specify the callback handler
public synchronized boolean setCallback(Callback callback) {
if (state != STOPPED)
return false;
this.callback = callback;
return true;
}
// Specify the number of audio channels
public synchronized int setChannels(int channels) {
return this.channels = state == STOPPED && (channels - 1 & ~1) != 0 ?
channels : this.channels;
}
// Specify the audio delay
public synchronized double setDelay(double delay) {
return this.delay = state == STOPPED && delay > 0 ? delay : this.delay;
}
// Specify the frame rate
public synchronized double setRate(double rate) {
return this.rate = state == STOPPED && rate > 0 ? rate : this.rate;
}
// Specify the audio sampling rate
public synchronized int setSampleRate(int sampleRate) {
return this.sampleRate = state == STOPPED && sampleRate > 0 ?
sampleRate : this.sampleRate;
}
// Begin timer operations
public synchronized boolean start(int source) {
// Error checking
if (source != CLOCK && source != AUDIO || state != STOPPED ||
callback == null || Thread.currentThread() == timer ||
source == AUDIO && buffer > delay) {
return false;
}
// Audio processing
if (source == AUDIO) try {
// Open the output line
AudioFormat fmt =
new AudioFormat(sampleRate, 16, channels, true, false);
line = AudioSystem.getSourceDataLine(fmt);
line.open(fmt);
// Configure audio fields
frames = audioFrames(rate);
block = 0;
blocks = new byte[(int) Math.ceil(delay / rate)]
[frames * channels * 2];
}
// Could not open the audio line
catch (Exception e) {
line = null;
return false;
}
// Configure state
this.source = source;
state = RUNNING;
// Spawn and start timer thread
cycTimer.reset();
timer = new Thread(()->timer());
timer.setDaemon(true);
timer.start();
// Spawn and start audio thread
if (source == AUDIO) {
cycAudio.reset();
audio = new Thread(()->audio());
audio.setDaemon(true);
audio.start();
}
// Synchronize with timer thread
try { cycTimer.await(); } catch (Exception e) { }
return true;
}
// End timer operations
public synchronized boolean stop() {
boolean isTimer = Thread.currentThread() == timer;
// The timer thread already owns the lock
if (isTimer && lock.isHeldByCurrentThread())
return true;
// Obtain the lock
lock.lock();
// Invalid state
if (state == STOPPED) {
lock.unlock();
return true;
}
// Configure state
boolean paused = state == PAUSED;
state = STOPPED;
// Stop timer thread
if (!isTimer) try {
if (!paused) timer.interrupt();
lock.unlock();
if ( paused) cycTimer.await();
cycTimer.await();
} catch (Exception e) { }
return true;
}
// Write audio samples as bytes to output
public boolean write(byte[] samples, int offset) {
int size = frames * channels * 2;
// Error checking
if (Thread.currentThread() != timer || state == STOPPED || written ||
samples == null || offset < 0 || samples.length < offset + size)
return false;
// Add a new sample block to the queue
block = (block + 1) % blocks.length;
byte[] block = blocks[this.block];
System.arraycopy(samples, offset, block, 0, size);
queue.offer(block);
return written = true;
}
// Write audio samples as shorts to output
public boolean write(short[] samples, int offset) {
int size = frames * channels;
// Error checking
if (Thread.currentThread() != timer || state == STOPPED || written ||
samples == null || offset < 0 || samples.length < offset + size)
return false;
// Encode the samples as bytes
block = (block + 1) % blocks.length;
byte[] block = blocks[this.block];
for (int src = 0, dest = 0; src < size; src++) {
short sample = samples[offset + src];
block[dest++] = (byte) sample;
block[dest++] = (byte) (sample >> 8);
}
queue.offer(block);
return written = true;
}
// Write audio samples as floats to output (range -1 to +1)
public boolean write(float[] samples, int offset) {
int size = frames * channels;
// Error checking
if (Thread.currentThread() != timer || state == STOPPED || written ||
samples == null || offset < 0 || samples.length < offset + size)
return false;
// Encode the samples as bytes
block = (block + 1) % blocks.length;
byte[] block = blocks[this.block];
for (int src = 0, dest = 0; src < size; src++) {
short sample = (short) Math.round(32767 *
Math.min(1, Math.max(-1, samples[offset + src])) );
block[dest++] = (byte) sample;
block[dest++] = (byte) (sample >> 8);
}
queue.offer(block);
return written = true;
}
///////////////////////////////////////////////////////////////////////////
// Private Methds //
///////////////////////////////////////////////////////////////////////////
// Calculate the number of audio sampling frames in some number of seconds
private int audioFrames(double seconds) {
return Math.max(1, (int) Math.round(seconds * sampleRate));
}
// Handler for pause operations -- invoked by timer thread
private long onPause(long reference, boolean isCallback) {
// Track the current time
if (source == CLOCK)
reference = System.nanoTime() - reference;
// Pause audio thread
else try {
audio.interrupt();
line.stop();
cycAudio.await();
} catch (Exception e) { }
// Synchronization
try {
if (!isCallback)
cycTimer.await(); // Synchronize with invoking thread
else lock.unlock();
cycTimer.await(); // Wait for resume() or stop()
} catch (Exception e) { }
// Calculate a new reference time
if (source == CLOCK)
reference = System.nanoTime() - reference;
// Unpause audio thread
else try { cycAudio.await(); } catch (Exception e) { }
return reference;
}
// Handler for stop operations -- invoked by timer thread
private int onStop(boolean paused, boolean isCallback) {
// Stop the audio thread
if (source == AUDIO) try {
audio.interrupt();
line.stop();
cycAudio.await();
} catch (Exception e) { }
// Synchronization
try {
if (!isCallback || paused)
cycTimer.await(); // Synchronize with invoking thread
else lock.unlock();
} catch (Exception e) { }
// Cleanup
blocks = null;
timer = null;
return 0;
}
///////////////////////////////////////////////////////////////////////////
// Thread Methods //
///////////////////////////////////////////////////////////////////////////
// Audio thread entry point
private void audio() {
// Synchronize with timer thread
try { cycAudio.await(); } catch (Exception e) { }
// Initialize working variables
byte[] block = new byte[audioFrames(delay ) * channels * 2];
byte[] buffer = new byte[audioFrames(this.buffer) * channels * 2];
int blockPos = 0;
int bufferPos = -buffer.length; // Less than 0 means not full
// Audio processing
line.start();
for (;;) {
// Fill the buffer with samples, blocking until full
while (bufferPos < 0) {
// Load bytes from the current block
if (blockPos < block.length) {
int size = Math.min(block.length - blockPos, -bufferPos);
System.arraycopy(block, blockPos, buffer,
bufferPos + buffer.length, size);
blockPos += size;
bufferPos += size;
}
// Fetch a new sample block, blocking until one is available
else try {
block = queue.take();
blockPos = 0;
}
// The timer state has changed
catch (Exception e) {
audio.interrupt(); // take() clears interrupt status
break;
}
}
// Send samples to the output, blocking until sent
if (!audio.isInterrupted()) {
bufferPos +=
line.write(buffer, bufferPos, buffer.length - bufferPos);
if (bufferPos == buffer.length)
bufferPos = -buffer.length;
}
// Check for changes to the timer state
if (state == RUNNING)
continue;
Thread.interrupted();
// Timer has paused
if (state == PAUSED) try {
cycAudio.await(); // Synchronize with timer thread
cycAudio.await(); // Wait for resume() or stop()
} catch (Exception e) { }
// Timer has stopped
if (state == STOPPED) {
try { line.close(); } catch (Exception e) { }
try { cycAudio.await(); } catch (Exception e) { }
audio = null;
return;
}
// Resume playback
line.start();
}
}
// Timer thread entry point
private int timer() {
// Synchronize with other threads
try {
if (source == AUDIO)
cycAudio.await(); // Synchronize with audio thread
cycTimer.await(); // Synchronize with parent thread
} catch (Exception e) { }
// Initialize working variables
byte[] empty = new byte[frames * channels * 2];
long reference = source == AUDIO ? 0 : System.nanoTime();
long target = source == AUDIO ? audioFrames(rate) :
Math.max(1, (long) Math.round(rate * 1000000000L));
// Timer processing
for (boolean first = true;; first = false) {
long current = source == CLOCK ? System.nanoTime() :
line.getLongFramePosition();
long remain = target - current + reference;
// Processing on all frames but the first
if (!first) {
// Wait until the next frame interval
if (remain > 0) try {
if (source == AUDIO)
remain = remain * 1000000000L / sampleRate;
remain = Math.max(1000000, remain);
Thread.sleep(remain / 1000000, (int) (remain % 1000000));
continue;
} catch (Exception e) { timer.interrupt(); }
// Another thread configured the timer state
if (Thread.interrupted()) {
if (state == PAUSED)
reference = onPause(reference, false);
if (state == STOPPED)
return onStop(false, false);
continue;
}
}
// Invoke the event callback
written = false;
callback.onFrame(this);
if (source == AUDIO && !written)
queue.offer(empty);
// The callback configured the timer state
if (lock.isHeldByCurrentThread()) {
boolean paused = state == PAUSED;
if (state == PAUSED)
reference = onPause(reference, true);
if (state == STOPPED)
return onStop(paused, true);
continue;
}
// Track processed frame time
reference = current + remain;
}
}
}

View File

@ -0,0 +1,249 @@
package util;
// Java imports
import java.io.*;
// Little-endian implementation of DataInputStream
public class LEDataInputStream extends FilterInputStream {
// Instance fields
private DataInputStream data; // Data-decoding stream
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public LEDataInputStream(InputStream stream) {
super(stream = stream instanceof LEDataInputStream ?
((LEDataInputStream) stream).in : stream);
data = stream instanceof DataInputStream ?
(DataInputStream) stream : new DataInputStream(stream);
}
// Byte array constructor
public LEDataInputStream(byte[] data) {
this(new ByteArrayInputStream(data));
}
///////////////////////////////////////////////////////////////////////////
// Public Methods //
///////////////////////////////////////////////////////////////////////////
// Reads a boolean from the underlying input stream
public boolean readBoolean() throws IOException {
return data.readByte() != 0;
}
// Reads an array of booleans from the underlying input stream
public boolean[] readBooleans(int count) throws IOException {
return readBooleans(new boolean[count], 0, count);
}
// Reads an array of booleans from the underlying input stream
public boolean[] readBooleans(boolean[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readBoolean();
return v;
}
// Reads a byte from the underlying input stream
public byte readByte() throws IOException {
return data.readByte();
}
// Reads an array of bytes from the underlying input stream
public byte[] readBytes(int count) throws IOException {
return readBytes(new byte[count], 0, count);
}
// Reads an array of bytes from the underlying input stream
public byte[] readBytes(byte[] v, int offset, int length)
throws IOException {
data.read(v, offset, length);
return v;
}
// Reads a character from the underlying input stream
public char readChar() throws IOException {
return (char) Short.reverseBytes(data.readShort());
}
// Reads an array of characters from the underlying input stream
public char[] readChars(int count) throws IOException {
return readChars(new char[count], 0, count);
}
// Reads an array of characters from the underlying input stream
public char[] readChars(char[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readChar();
return v;
}
// Reads a double from the underlying input stream
public double readDouble() throws IOException {
return readDouble(false);
}
// Reads a double from the underlying input stream
public double readDouble(boolean finite) throws IOException {
long bits = readLong();
double ret = Double.longBitsToDouble(bits);
if (finite && !Double.isFinite(ret)) throw new RuntimeException(
String.format("Non-finite double value 0x%016X", bits));
return ret;
}
// Reads an array of doubles from the underlying input stream
public double[] readDoubles(int count) throws IOException {
return readDoubles(new double[count], 0, count, false);
}
// Reads an array of doubles from the underlying input stream
public double[] readDoubles(int count, boolean finite) throws IOException {
return readDoubles(new double[count], 0, count, finite);
}
// Reads an array of doubles from the underlying input stream
public double[] readDoubles(double[] v, int offset, int length)
throws IOException {
return readDoubles(v, offset, length, false);
}
// Reads an array of doubles from the underlying input stream
public double[] readDoubles(double[] v, int offset, int length,
boolean finite) throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readDouble(finite);
return v;
}
// Reads a float from the underlying input stream
public float readFloat() throws IOException {
return readFloat(false);
}
// Reads a float from the underlying input stream
public float readFloat(boolean finite) throws IOException {
int bits = readInt();
float ret = Float.intBitsToFloat(bits);
if (finite && !Float.isFinite(ret)) throw new RuntimeException(
String.format("Non-finite float value 0x%08X", bits));
return ret;
}
// Reads an array of floats from the underlying input stream
public float[] readFloats(int count) throws IOException {
return readFloats(new float[count], 0, count, false);
}
// Reads an array of floats from the underlying input stream
public float[] readFloats(int count, boolean finite) throws IOException {
return readFloats(new float[count], 0, count, finite);
}
// Reads an array of floats from the underlying input stream
public float[] readFloats(float[] v, int offset, int length)
throws IOException {
return readFloats(v, offset, length, false);
}
// Reads an array of floats from the underlying input stream
public float[] readFloats(float[] v, int offset, int length,
boolean finite) throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readFloat(finite);
return v;
}
// Reads an int from the underlying input stream
public int readInt() throws IOException {
return Integer.reverseBytes(data.readInt());
}
// Reads an array of ints from the underlying input stream
public int[] readInts(int count) throws IOException {
return readInts(new int[count], 0, count);
}
// Reads an array of ints from the underlying input stream
public int[] readInts(int[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readInt();
return v;
}
// Reads a long from the underlying input stream
public long readLong() throws IOException {
return Long.reverseBytes(data.readLong());
}
// Reads an array of longs from the underlying input stream
public long[] readLongs(int count) throws IOException {
return readLongs(new long[count], 0, count);
}
// Reads an array of longs from the underlying input stream
public long[] readLongs(long[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readLong();
return v;
}
// Reads a 24-bit integer from the underlying input stream
public int readMiddle() throws IOException {
int v = readShort() & 0xFFFF;
return readByte() << 16 | v;
}
// Reads an array of 24-bit integers from the underlying input stream
public int[] readMiddles(int count) throws IOException {
return readMiddles(new int[count], 0, count);
}
// Reads an array of 24-bit integers from the underlying input stream
public int[] readMiddles(int[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readMiddle();
return v;
}
// Reads a short from the underlying input stream
public short readShort() throws IOException {
return Short.reverseBytes(data.readShort());
}
// Reads an array of shorts from the underlying input stream
public short[] readShorts(int count) throws IOException {
return readShorts(new short[count], 0, count);
}
// Reads an array of shorts from the underlying input stream
public short[] readShorts(short[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
v[offset + x] = readShort();
return v;
}
// Reads from the stream in a modified UTF-8 string
public String readUTF() throws IOException {
return data.readUTF();
}
// Advances forward in the stream some number of bytes
public int skipBytes(int n) throws IOException {
return data.skipBytes(n);
}
}

View File

@ -0,0 +1,218 @@
package util;
// Java imports
import java.io.*;
import java.nio.charset.*;
// Little-endian implementation of DataOutputStream
public class LEDataOutputStream extends FilterOutputStream {
// Instance fields
private DataOutputStream data; // Data-encoding stream
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public LEDataOutputStream() {
this(new ByteArrayOutputStream());
}
// Stream constructor
public LEDataOutputStream(OutputStream stream) {
super(stream = stream instanceof LEDataOutputStream ?
((LEDataOutputStream) stream).out : stream);
data = stream instanceof DataOutputStream ?
(DataOutputStream) stream : new DataOutputStream(stream);
}
///////////////////////////////////////////////////////////////////////////
// Public Methods //
///////////////////////////////////////////////////////////////////////////
// Returns the current value of the underlying buffer
public int size() {
return out instanceof ByteArrayOutputStream ?
((ByteArrayOutputStream) out).size() : -1;
}
// Produce a byte array containing this stream's data
public byte[] toByteArray() throws IOException {
return out instanceof ByteArrayOutputStream ?
((ByteArrayOutputStream) out).toByteArray() : null;
}
// Writes a boolean to the underlying output stream as a 1-byte value
public void writeBoolean(boolean v) throws IOException {
data.writeBoolean(v);
}
// Writes an array of booleans to the underlying output stream
public void writeBooleans(boolean[] v) throws IOException {
writeBooleans(v, 0, v.length);
}
// Writes an array of booleans to the underlying output stream
public void writeBooleans(boolean[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeBoolean(v[offset + x]);
}
// Writes out a byte to the underlying output stream as a 1-byte value
public void writeByte(int v) throws IOException {
data.writeByte(v);
}
// Writes out the string to the underlying output stream
public void writeBytes(String s) throws IOException {
data.writeBytes(s);
}
// Writes an array of bytes to the underlying output stream
public void writeBytes(byte[] v) throws IOException {
writeBytes(v, 0, v.length);
}
// Writes an array of bytes to the underlying output stream
public void writeBytes(byte[] v, int offset, int length)
throws IOException {
data.write(v, offset, length);
}
// Writes a character to the underlying output stream as a 2-byte value
public void writeChar(int v) throws IOException {
data.writeChar(Short.reverseBytes((short) v));
}
// Writes a string to the underlying output stream
public void writeChars(String s) throws IOException {
writeChars(s.toCharArray());
}
// Writes an array of characters to the underlying output stream
public void writeChars(char[] v) throws IOException {
writeChars(v, 0, v.length);
}
// Writes an array of characters to the underlying output stream
public void writeChars(char[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeChar(v[offset + x]);
}
// Converts the double argument to a long using doubleToLongBits
public void writeDouble(double v) throws IOException {
data.writeLong(Long.reverseBytes(Double.doubleToLongBits(v)));
}
// Writes an array of doubles to the underlying output stream
public void writeDoubles(double[] v) throws IOException {
writeDoubles(v, 0, v.length);
}
// Writes an array of doubles to the underlying output stream
public void writeDoubles(double[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeDouble(v[offset + x]);
}
// Converts the float argument to an int using floatToIntBits
public void writeFloat(float v) throws IOException {
data.writeInt(Integer.reverseBytes(Float.floatToIntBits(v)));
}
// Writes an array of floats to the underlying output stream
public void writeFloats(float[] v) throws IOException {
writeFloats(v, 0, v.length);
}
// Writes an array of floats to the underlying output stream
public void writeFloats(float[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeFloat(v[offset + x]);
}
// Writes an int to the underlying output stream as four bytes
public void writeInt(int v) throws IOException {
data.writeInt(Integer.reverseBytes(v));
}
// Writes an array of ints to the underlying output stream
public void writeInts(int[] v) throws IOException {
writeInts(v, 0, v.length);
}
// Writes an array of ints to the underlying output stream
public void writeInts(int[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeInt(v[offset + x]);
}
// Writes an int to the underlying output stream as three bytes
public void writeMiddle(int v) throws IOException {
writeShort((short) v );
writeByte ((byte ) (v >> 16));
}
// Writes an array of 24-bit integers to the underlying output stream
public void writeMiddles(int[] v) throws IOException {
writeMiddles(v, 0, v.length);
}
// Writes an array of 24-bit integers to the underlying output stream
public void writeMiddles(int[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeMiddle(v[offset + x]);
}
// Writes a long to the underlying output stream as eight bytes
public void writeLong(long v) throws IOException {
data.writeLong(Long.reverseBytes(v));
}
// Writes an array of longs to the underlying output stream
public void writeLong(long[] v) throws IOException {
writeLongs(v, 0, v.length);
}
// Writes an array of longs to the underlying output stream
public void writeLongs(long[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeLong(v[offset + x]);
}
// Writes a short to the underlying output stream as two bytes
public void writeShort(int v) throws IOException {
data.writeShort(Short.reverseBytes((short) v));
}
// Writes an array of shorts to the underlying output stream
public void writeShorts(short[] v) throws IOException {
writeShorts(v, 0, v.length);
}
// Writes an array of shorts to the underlying output stream
public void writeShorts(short[] v, int offset, int length)
throws IOException {
for (int x = 0; x < length; x++)
writeShort(v[offset + x]);
}
// Writes a string to the underlying output stream using modified UTF-8
public void writeUTF(String str) throws IOException {
data.writeUTF(str);
}
}

View File

@ -0,0 +1,433 @@
package util;
// Java imports
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.text.*;
// UI display text manager
public class Localizer {
// Instance fields
private HashMap<Object, Object> controls; // Control mapping
private HashMap<String, String> messages; // Message dictionary
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Class reference for JComboBox<String>
JComboBox<String> JCOMBOBOX = new JComboBox<String>();
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public Localizer() {
controls = new HashMap<Object, Object>();
messages = new HashMap<String, String>();
}
///////////////////////////////////////////////////////////////////////////
// Public Methods //
///////////////////////////////////////////////////////////////////////////
// Add a control to the collection
public boolean add(Object control, Object key) {
// Error checking
if (control == null || key == null)
return false;
// Control takes a single string
if (key instanceof String) {
if (!(
control instanceof AbstractButton ||
control instanceof JFrame ||
control instanceof JInternalFrame ||
control instanceof JPanel || // TitledBorder
control instanceof JTextComponent
)) return false;
}
// Control takes an array of strings
else if (key instanceof String[]) {
if (!(
JCOMBOBOX.getClass().isAssignableFrom(control.getClass())
)) return false;
}
// Invalid control type
else return false;
// Add the control to the collection
controls.put(control, key);
update();
return true;
}
// Remove all controls from being managed
public void clearControls() {
controls.clear();
}
// Configure a dictionary entry
public String put(String key, String value) {
String ret = value == null ?
messages.remove(key) :
messages.put(key, value)
;
update();
return ret;
}
// Remove a control from the collection
public boolean remove(Object control) {
return controls.remove(control) != null;
}
// Specify a message dictionary
public void set(String text) {
// Pre-processing
messages.clear();
if (text == null) {
update();
return;
}
// Configure working variables
var chars = text.replaceAll("\\r\\n?", "\n").toCharArray();
int col = 1; // Current character on the current line
int line = 1; // Current line in the file
int start = 0; // Position of first character in key or value
var stack = new Stack<String>(); // Nested key chain
int state = 0; // Current parsing context
// Process all characters
for (int x = 0; x < chars.length; x++, col++) {
char c = chars[x];
boolean end = x == chars.length - 1;
String pos = line + ":" + col + ": ";
boolean white = c == ' ' || c == '\t';
// Comment
if (state == -1) {
if (c != '\n' && !end)
continue;
col = 1;
state = 0;
line++;
}
// Pre-key
else if (state == 0) {
// Ignore leading whitespace
if (white)
continue;
// The line is a comment
if (c == '#') {
state = -1;
continue;
}
// End of input has been reached
if (end)
break;
// The line is empty
if (c == '\n') {
col = 1;
line++;
continue;
}
// Proceed to key
start = x;
state = 1;
}
// Key
else if (state == 1) {
// Any non-whitespace character is valid in a key
if (!white && c != '\n' && !end)
continue;
// Produce the key as a string
String key = new String(chars, start, x - start).trim();
// Close a key group
if (key.equals("}")) {
// Nesting error
if (stack.size() == 0) throw new RuntimeException(
pos + "Unexpected '}'");
// Syntax error
outer: for (int y = x; y < chars.length; y++) {
char h = chars[y];
if (h == '\n') break;
if (h != ' ' && h != '\t') throw new RuntimeException(
pos + "Newline expected");
}
// Proceed to key
state = 0;
stack.pop();
continue;
}
// Curly braces are not valid in keys
if (key.contains("{") || key.contains("}"))
throw new RuntimeException(
line + ": Curly braces are not allowed in keys");
// Proceed to interim
stack.push(key);
state = 2;
}
// Post-key, pre-value
if (state == 2) {
// Ignore any whitespace between key and value
if (white)
continue;
// No value is present
if (c == '\n') throw new RuntimeException(
pos + "Unexpected newline");
if (end) throw new RuntimeException(
pos + "Unexpected end of input");
// Proceed to value
start = x;
state = 3;
}
// Value
if (state == 3) {
// Escape sequence
if (c == '\\') {
// EOF
if (x == chars.length - 1)
throw new RuntimeException(
pos + "Unexpected end of input");
// Cannot escape a newline
if (chars[x + 1] == '\n')
throw new RuntimeException(
pos + "Unexpected newline");
// Skip the next character
x++;
continue;
}
// The end of the value has not yet been reached
if (c != '\n' && !end)
continue;
// Produce the value as a string
if (end)
x++;
String value = new String(chars, start, x - start).trim();
col = 1;
state = 0;
line++;
// Open a key group
if (value.equals("{")) {
state = 0;
continue;
}
// Check for nesting errors
int depth = 0;
for (int y = start; y < x; y++) {
switch (chars[y]) {
case '\\': y++; continue;
case '{' : depth++; continue;
case '}' : if (--depth == -1)
throw new RuntimeException(
line + ":" + (x - start + y + 1) +
": Unexpected '}'"
);
continue;
}
}
if (depth != 0) throw new RuntimeException(
pos + "'}' expected");
// Produce the full key as a string
var pkey = new StringBuilder();
for (String item : stack)
pkey.append((pkey.length() != 0 ? "." : "") + item);
String key = pkey.toString();
// Check for duplicate keys
String lkey = key.toLowerCase();
if (messages.get(lkey) != null) throw new RuntimeException(
(line-1)+ ": Key '" + key + "' has already been defined.");
// Add the pair to the dictionary
messages.put(lkey, value);
stack.pop();
}
}
// Update all controls
update();
}
///////////////////////////////////////////////////////////////////////////
// Private Methods //
///////////////////////////////////////////////////////////////////////////
// Process substitutions and escapes on a message for a given key
private String evaluate(String key) {
String ret = messages.get(key.toLowerCase());
// The topmost key does not exist in the dictionary
if (ret == null)
return null;
// Perform all substitutions
outer: for (;;) {
var chars = ret.toCharArray();
int start = 0;
// Look for the first complete substitution
for (int x = 0; x < chars.length; x++) {
char c = chars[x];
// Escape sequence
if (c == '\\') {
x++;
continue;
}
// Begin substitution
if (c == '{')
start = x + 1;
// Substitution has not yet ended
if (c != '}')
continue;
// Determine the substitution
key = new String(chars, start, x - start);
String value = messages.get(key.toLowerCase());
if (value == null)
value = "\\{" + key + "\\}";
// Apply the substitution to the message
ret =
new String(chars, 0, start - 1) +
value +
new String(chars, x + 1, chars.length - x - 1)
;
continue outer;
}
// No further substitutions
break;
}
// Unescape all escape sequences
var chars = ret.toCharArray();
int length = 0;
for (int x = 0; x < chars.length; x++, length++) {
char c = chars[x];
if (c == '\\') switch (c = chars[++x]) {
case 'n': c = '\n'; break;
case 't': c = '\t'; break;
}
chars[length] = c;
}
return new String(chars, 0, length);
}
// Update the text for all controls
private void update() {
// Process all controls
for (var control : controls.keySet()) {
Object key = controls.get(control);
String[] values = null;
// One string
if (key instanceof String)
values = new String[] { evaluate((String) key) };
// Multiple strings
else {
String[] keys = (String[]) key;
values = new String[keys.length];
for (int x = 0; x < keys.length; x++)
values[x] = evaluate(keys[x]);
}
// Update the control's text
if (control instanceof AbstractButton)
((AbstractButton) control).setText (values[0]);
if (control instanceof JFrame)
((JFrame ) control).setTitle(values[0]);
if (control instanceof JInternalFrame)
((JInternalFrame) control).setTitle(values[0]);
if (control instanceof JTextComponent)
((JTextComponent) control).setText (values[0]);
// JPanel must be wrapped in a TitledBorder
if (control instanceof JPanel) {
var border = ((JPanel) control).getBorder();
if (border instanceof TitledBorder)
((TitledBorder) border).setTitle(values[0]);
}
// Replace the contents of a JComboBox without firing events
if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) {
// The type is explicitly verified above
@SuppressWarnings("unchecked")
var box = (JComboBox<String>) control;
// Configure working variables
var action = box.getActionListeners();
int index = box.getSelectedIndex();
var item = box.getItemListeners();
// Remove event listeners
for (var lst : action) box.removeActionListener(lst);
for (var lst : item ) box.removeItemListener (lst);
// Update contents
box.setModel(new DefaultComboBoxModel<String>(values));
box.setSelectedIndex(index);
// Restore event listeners
for (var lst : action) box.addActionListener(lst);
for (var lst : item ) box.addItemListener (lst);
}
}
}
}

284
src/desktop/util/Util.java Normal file
View File

@ -0,0 +1,284 @@
package util;
// Java imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.imageio.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;
// General utility methods
public final class Util {
// This class cannot be instantiated
private Util() { }
///////////////////////////////////////////////////////////////////////////
// Types //
///////////////////////////////////////////////////////////////////////////
// Event listener interfaces
public interface OnClose { void call(WindowEvent e); }
public interface OnClose2 { void call(InternalFrameEvent e); }
public interface OnFocus { void call(FocusEvent e); }
public interface OnKey { void call(KeyEvent e); }
public interface OnMouse { void call(MouseEvent e); }
public interface OnResize { void call(ComponentEvent e); }
// Data class for byte-order marks
private static class BOM {
byte[] mark;
Charset set;
BOM(byte[] m, Charset s) { mark = m; set = s; }
}
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Text file byte-order marks
private static final BOM[] BOMS = {
new BOM(new byte[] { -17, -69, -65 }, StandardCharsets.UTF_8 ),
new BOM(new byte[] { - 2, - 1 }, StandardCharsets.UTF_16BE),
new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE),
};
///////////////////////////////////////////////////////////////////////////
// Static Methods //
///////////////////////////////////////////////////////////////////////////
// Apply an alpha value to a Color object
public static Color alpha(Color color, float alpha) {
int a = (int) Math.round(alpha * 255);
return color == null || a < 0 || a > 255 ? null :
new Color(color.getRGB() & 0x00FFFFFF | a << 24, true);
}
// Blend one color into another
public static Color blend(Color front, Color back, float alpha) {
// Error checking
if (front == null || back == null ||
!Float.isFinite(alpha) || alpha < 0 || alpha > 1)
return null;
// Decompose components
var colors = new Color[] { front, back };
var argb = new float[3][4];
for (int x = 0; x < 2; x++) {
int bits = colors[x].getRGB();
for (int y = 3; y >= 0; y++, bits >>= 8)
argb[x][y] = (bits & 0xFF) / 255.0f;
}
// Combine the colors
argb[2][0] = argb[0][0] + argb[1][0] * (1 - argb[0][0]);
for (int x = 1; x < 4; x++)
argb[2][x] = (
argb[0][x] * argb[0][0] +
argb[1][x] * argb[1][0] * (1 - argb[0][0])
) / argb[2][0];
// Produce the resulting Color object
int bits = 0;
for (int x = 0; x < 4; x++)
bits = bits << 8 | (int) Math.round(argb[2][x] * 255);
return new Color(bits, true);
}
// Read a file, first from disk, then from .jar
public static byte[] fileRead(String filename) {
InputStream stream = null;
// Open the file on disk
try { stream = new FileInputStream(filename); }
// File on disk could not be opened, so get resource from .jar
catch (Exception e) {
stream = Util.class.getResourceAsStream("/" + filename);
if (stream == null)
return null; // Resource in .jar could not be found
}
// Read the file data into memory
byte[] data = null;
try { data = stream.readAllBytes(); } catch (Exception e) { }
try { stream.close(); } catch (Exception e) { }
return data;
}
// Read an image file as an icon
public static Icon iconRead(String filename) {
BufferedImage img = imageRead(filename);
return img == null ? null : new ImageIcon(img);
}
// Read an image file by filename
public static BufferedImage imageRead(String filename) {
try { return
ImageIO.read(new ByteArrayInputStream(fileRead(filename)));
} catch (Exception e) { return null; }
}
// Read an image file by handle
public static BufferedImage imageRead(File file) {
try { return imageRead(file.getAbsolutePath()); }
catch (Exception e) { return null; }
}
// Produce a WindowListener with a functional interface
public static WindowListener onClose(OnClose close) {
return new WindowListener() {
public void windowActivated (WindowEvent e) { }
public void windowClosed (WindowEvent e) { }
public void windowDeactivated(WindowEvent e) { }
public void windowDeiconified(WindowEvent e) { }
public void windowIconified (WindowEvent e) { }
public void windowOpened (WindowEvent e) { }
public void windowClosing (WindowEvent e)
{ if (close != null) close.call(e); }
};
}
// Produce an InternalFrameListener with a functional interface
public static InternalFrameListener onClose2(OnClose2 close) {
return new InternalFrameListener() {
public void internalFrameActivated (InternalFrameEvent e) { }
public void internalFrameClosed (InternalFrameEvent e) { }
public void internalFrameDeactivated(InternalFrameEvent e) { }
public void internalFrameDeiconified(InternalFrameEvent e) { }
public void internalFrameIconified (InternalFrameEvent e) { }
public void internalFrameOpened (InternalFrameEvent e) { }
public void internalFrameClosing (InternalFrameEvent e)
{ if (close != null) close.call(e); }
};
}
// Produce a FocusListener with functional interfaces
public static FocusListener onFocus(OnFocus focus, OnFocus blur) {
return new FocusListener() {
public void focusGained(FocusEvent e)
{ if (focus != null) focus.call(e); }
public void focusLost (FocusEvent e)
{ if (blur != null) blur .call(e); }
};
}
// Produce a KeyListener with functional interfaces
public static KeyListener onKey(OnKey down, OnKey up) {
return new KeyListener() {
public void keyTyped (KeyEvent e) { }
public void keyPressed (KeyEvent e)
{ if (down != null) down.call(e); }
public void keyReleased(KeyEvent e)
{ if (up != null) up .call(e); }
};
}
// Produce a MouseMotionListener with a functional interface
public static MouseMotionListener onMouseMove(OnMouse move, OnMouse drag) {
return new MouseMotionListener() {
public void mouseMoved (MouseEvent e)
{ if (move != null) move.call(e); }
public void mouseDragged(MouseEvent e)
{ if (drag != null) drag.call(e); }
};
}
// Produce a MouseListener with functional interfaces
public static MouseListener onMouse(OnMouse down, OnMouse up) {
return new MouseListener() {
public void mouseClicked (MouseEvent e) { }
public void mouseEntered (MouseEvent e) { }
public void mouseExited (MouseEvent e) { }
public void mousePressed (MouseEvent e)
{ if (down != null) down.call(e); }
public void mouseReleased(MouseEvent e)
{ if (up != null) up .call(e); }
};
}
// Produce a ComponentListener with a functional interface
public static ComponentListener onResize(OnResize resize) {
return new ComponentListener() {
public void componentHidden (ComponentEvent e) { }
public void componentMoved (ComponentEvent e) { }
public void componentShown (ComponentEvent e) { }
public void componentResized(ComponentEvent e)
{ if (resize != null) resize.call(e); }
};
}
// Configure the Swing look-and-feel with the system theme
public static boolean setSystemLAF() {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
return true;
} catch (Exception e) { return false; }
}
// Produce a JSplitPane with an un-styled divider
public static JSplitPane splitPane(int orientation) {
var split = new JSplitPane(orientation, null, null);
split.setContinuousLayout(true);
split.setDividerSize(2);
split.setUI(new BasicSplitPaneUI() {
public BasicSplitPaneDivider createDefaultDivider() {
return new BasicSplitPaneDivider(this) {
public void setBorder(Border b) { }
};
}
});
split.setBorder(null);
return split;
}
// Read a text file, accounting for BOMs and guaranteeing Unix line endings
public static String textRead(String filename) {
// Read the file
byte[] data = fileRead(filename);
if (data == null)
return null;
// Configure working variables
byte[] mark = null;
Charset set = StandardCharsets.ISO_8859_1; // Default charset
// Check for a byte-order mark
outer: for (BOM bom : BOMS) {
if (data.length < bom.mark.length)
continue;
for (int x = 0; x < bom.mark.length; x++)
if (data[x] != bom.mark[x])
continue outer;
mark = bom.mark;
set = bom.set;
break;
}
// Remove the byte-order mark from the data
if (mark != null) {
byte[] temp = new byte[data.length - mark.length];
System.arraycopy(data, mark.length, temp, 0, temp.length);
data = temp;
}
// Produce a string with LF-style (Unix) line endings
return new String(data, set).replaceAll("\\r\\n?", "\n");
}
}

137
src/desktop/util/XML.java Normal file
View File

@ -0,0 +1,137 @@
package util;
// Java imports
import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
// Utility methods for managing XML documents
public class XML {
// This class cannot be instantiated
private XML() { }
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Parsing instance
private static final DocumentBuilder XML_PARSER;
// Static initializer
static {
DocumentBuilder parser = null;
try { parser =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (Exception e) { }
XML_PARSER = parser;
}
///////////////////////////////////////////////////////////////////////////
// Static Methods //
///////////////////////////////////////////////////////////////////////////
// Retrieve an attribute value from an XML element
public static String attribute(Element element, String attribute) {
return element.hasAttribute(attribute) ?
element.getAttribute(attribute) : null;
}
// Retrieve the child elements of an XML node
public static Element[] children(Node node) {
var nodes = node.getChildNodes();
int count = nodes.getLength();
var ret = new ArrayList<Element>();
for (int x = 0; x < count; x++) {
node = nodes.item(x);
if (node.getNodeType() == Node.ELEMENT_NODE)
ret.add((Element) node);
}
return ret.toArray(new Element[ret.size()]);
}
// Retrieve the first node matching the given hierarchy
public static Element element(Node node, String hierarchy) {
// Error checking
if (node == null || hierarchy == null)
return null;
// Propagate down the hierarchy
outer: for (String name : hierarchy.split("\\.")) {
var nodes = node.getChildNodes();
int count = nodes.getLength();
for (int x = 0; x < count; x++) {
node = nodes.item(x);
if (
node.getNodeType() == Node.ELEMENT_NODE &&
node.getNodeName().equals(name)
) continue outer;
}
return null;
}
return (Element) node;
}
// Retrieve all nodes matching the endpoint of a given hierarchy
public static Element[] elements(Node node, String hierarchy) {
// Find the first matching node
node = element(node, hierarchy);
if (node == null)
return new Element[0];
// Working variables
var names = hierarchy.split("\\.");
String name = names[names.length - 1];
var ret = new ArrayList<Element>();
// Include all siblings with the same tag name
ret.add((Element) node);
for (;;) {
node = node.getNextSibling();
if (node == null)
break;
if (
node.getNodeType() == Node.ELEMENT_NODE &&
node.getNodeName().equals(name)
) ret.add((Element) node);
}
return ret.toArray(new Element[ret.size()]);
}
// Read and parse an XML file
public static Node read(String filename) {
try { return XML_PARSER.parse(
new ByteArrayInputStream(Util.fileRead(filename)));
} catch (Exception e) { return null; }
}
// Retrieve the text content of an element
public static String text(Node node) {
// Error checking
if (node == null)
return null;
// Find the child node called #text
var nodes = node.getChildNodes();
int count = nodes.getLength();
for (int x = 0; x < count; x++) {
node = nodes.item(x);
if (node.getNodeType() != Node.TEXT_NODE)
continue;
String text = node.getNodeValue();
return text == null ? "" : text.trim();
}
return "";
}
}