diff --git a/README.md b/README.md index 314feb1..0a3e5ee 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ version numbers.* * **PlutoShader** - Stable * **PlutoSpriteSheet** - Stable, some features are unfinished * **PlutoStatic** - Stable, collision API nowhere near completion + * **PlutoUSS2** - Stable ### Unstable submodules * **PlutoAudio** - Somewhat usable, unfinished @@ -64,7 +65,7 @@ See `NEXT_RELEASE_DRAFT.md` for details. [ *Items not required immediately, planned to be implemented eventually.* ] * Allow multiple running instances of Pluto * Alternatively, if this deems too difficult to implement, - prohibit the creation of more than instance per VM to avoid issues + prohibit the creation of more than one instance per JVM to avoid issues * A networking API * Expand upon the Color API * Color mixing and blending diff --git a/UPDATE_NOTES.md b/UPDATE_NOTES.md index d4eb328..0c0e43c 100644 --- a/UPDATE_NOTES.md +++ b/UPDATE_NOTES.md @@ -1,4 +1,6 @@ ## 20.2.0.0-alpha.3 +* `[PlutoUSS2]` **Added USS2 as a new module** + * `[PlutoLib]` `PlutoLib` now depends on `PlutoUSS2` * `[PlutoLib]` *Removed* `Severity`, use `SmartSeverity` instead * `[PlutoLib]` *Removed* `TextIn`, `TextOut`, `ResourceImage` and `ResourceInputStream` * `[PlutoLib]` *Removed* `StaticPlutoEventManager` as the implementation was too obscure @@ -18,7 +20,7 @@ * `[PlutoLib]` Created the Version API * Added the `IVersion` interface * Added support for version objects - * As a result, all fields except the version string are no longer compile-time constants + * As a result, all fields in `Pluto` except the version string are no longer compile-time constants * `[PlutoCore]` Made `PlutoApplication`'s constructor private * `[PlutoLib]` `MiniTimeParseException` no longer contains a hardcoded String message diff --git a/plutolib/build.gradle b/plutolib/build.gradle index 12f3b7b..e4928df 100644 --- a/plutolib/build.gradle +++ b/plutolib/build.gradle @@ -33,6 +33,8 @@ task generateConfigs(type: Copy) { compileJava.dependsOn generateConfigs dependencies { + api project(":plutouss2") + api platform("org.lwjgl:lwjgl-bom:$lwjglVersion") api "com.google.code.findbugs:jsr305:3.0.2" diff --git a/plutouss2/build.gradle b/plutouss2/build.gradle new file mode 100644 index 0000000..4779a11 --- /dev/null +++ b/plutouss2/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'java-library' + +description = "UniversalSerializationSystem 2 (formerly UserStorageSystem) is a simple library for " + + "in-memory serialization of basic data types to ByteBuffers with versioned schema support." + +dependencies { +} diff --git a/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/EnumUSS2PropertyType.java b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/EnumUSS2PropertyType.java new file mode 100644 index 0000000..d0a74aa --- /dev/null +++ b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/EnumUSS2PropertyType.java @@ -0,0 +1,10 @@ +package cz.tefek.pluto.uss2.properties; + +public enum EnumUSS2PropertyType +{ + BYTE, + INT, + LONG, + STRING, + DOUBLE +} diff --git a/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertyObject.java b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertyObject.java new file mode 100644 index 0000000..e7201b0 --- /dev/null +++ b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertyObject.java @@ -0,0 +1,104 @@ +package cz.tefek.pluto.uss2.properties; + +import java.nio.ByteBuffer; + +public final class USS2PropertyObject +{ + final USS2PropertySchema schema; + + final int currentVersion; + final int latestVersion; + boolean dirty; + + final ByteBuffer data; + + private USS2PropertyObject(ByteBuffer buf, USS2PropertySchema schema) + { + this.schema = schema; + + if (buf.limit() != schema.getCapacity()) + throw new IllegalArgumentException("The input ByteBuffer's limit and the USS2PropertySchema size must be the same value!"); + + this.currentVersion = 0xff & buf.get(0); + this.latestVersion = schema.getVersion(); + + if (this.currentVersion > this.latestVersion) + throw new RuntimeException(String.format("The file's version (%d) is newer than what USS2 can read (%d)!", this.currentVersion, this.latestVersion)); + + this.data = buf; + } + + private static USS2PropertyObject upgrade(USS2PropertyObject po) + { + var poNew = create(po.schema); + var props = po.schema.getProperties(); + props.forEach(property -> { + if (property instanceof USS2PropertySchema.USS2Int) + { + var ussInt = (USS2PropertySchema.USS2Int) property; + ussInt.write(poNew, ussInt.read(po)); + } + else if (property instanceof USS2PropertySchema.USS2Long) + { + var ussLong = (USS2PropertySchema.USS2Long) property; + ussLong.write(poNew, ussLong.read(po)); + } + else if (property instanceof USS2PropertySchema.USS2Double) + { + var ussDouble = (USS2PropertySchema.USS2Double) property; + ussDouble.write(poNew, ussDouble.read(po)); + } + else if (property instanceof USS2PropertySchema.USS2Byte) + { + var ussByte = (USS2PropertySchema.USS2Byte) property; + ussByte.write(poNew, ussByte.read(po)); + } + else if (property instanceof USS2PropertySchema.USS2String) + { + var ussString = (USS2PropertySchema.USS2String) property; + ussString.write(poNew, ussString.read(po)); + } + }); + + return poNew; + } + + public static USS2PropertyObject create(USS2PropertySchema schema) + { + var buf = ByteBuffer.wrap(new byte[schema.getCapacity()]); + buf.put(0, (byte) schema.getVersion()); + + var po = new USS2PropertyObject(buf, schema); + po.dirty = true; + + return po; + } + + public static USS2PropertyObject from(ByteBuffer buf, USS2PropertySchema schema) + { + var po = new USS2PropertyObject(buf, schema); + + if (po.latestVersion != po.currentVersion) + return upgrade(po); + + return po; + } + + public ByteBuffer getData() + { + return this.data; + } + + public byte[] getDataByteArray() + { + if (!this.data.hasArray()) + throw new RuntimeException("Data does not have a backing array!"); + + return this.data.array(); + } + + public boolean isDirty() + { + return this.dirty; + } +} diff --git a/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertySchema.java b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertySchema.java new file mode 100644 index 0000000..d2b4942 --- /dev/null +++ b/plutouss2/src/main/java/cz/tefek/pluto/uss2/properties/USS2PropertySchema.java @@ -0,0 +1,273 @@ +package cz.tefek.pluto.uss2.properties; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public abstract class USS2PropertySchema +{ + public static final int DOES_NOT_EXIST_IN_VERSION = -1; + public static final int MAX_VERSION = 0xff; + + private final List properties; + + private final int capacity; + + private int version; + private final int[] offsets; + + protected USS2PropertySchema(int capacity) + { + if (capacity < 16) + throw new IllegalArgumentException("Please use a starting capacity of at least 16 bytes."); + + this.properties = new ArrayList<>(); + this.offsets = new int[MAX_VERSION + 1]; + + // Version byte + Arrays.fill(this.offsets, Byte.BYTES); + + this.capacity = capacity; + this.version = 0; + } + + public final String getInfo() + { + float used = this.offsets[MAX_VERSION] / (float) this.capacity; + final int bars = 30; + int usedBars = Math.round(used * bars); + + return String.format("[%s%s] %.2f%% of schema used (%d/%d bytes).", "|".repeat(usedBars), " ".repeat(bars - usedBars), used * 100, this.offsets[MAX_VERSION], this.capacity); + } + + private void declareProperty(USS2Property property) + { + Arrays.fill(property.offsets, 0, property.version, DOES_NOT_EXIST_IN_VERSION); + System.arraycopy(this.offsets, property.version, property.offsets, property.version, property.offsets.length - property.version); + + // No need to check all versions, because the highest version is the largest by default (properties cannot be removed) + if (this.offsets[MAX_VERSION] + property.objectSize > this.capacity) + throw new IllegalStateException( + String.format("Error: Declaring another property (of size %d) " + + "would put the schema's total size to %d, which is over the capacity of %d.", + property.objectSize, + this.offsets[MAX_VERSION] + property.objectSize, + this.capacity)); + + this.properties.add(property); + + if (property.version > this.version) + this.version = property.version; + + for (int i = property.version; i <= MAX_VERSION; i++) + this.offsets[i] += property.objectSize; + } + + public final List getProperties() + { + return Collections.unmodifiableList(this.properties); + } + + public final int getVersion() + { + return this.version; + } + + public final int getCapacity() + { + return this.capacity; + } + + protected final USS2Int declareInt(byte version) + { + var ussInt = new USS2Int(0xff & version); + this.declareProperty(ussInt); + return ussInt; + } + + protected final USS2Long declareLong(byte version) + { + var ussLong = new USS2Long(0xff & version); + this.declareProperty(ussLong); + return ussLong; + } + + protected final USS2Double declareDouble(byte version) + { + var ussDouble = new USS2Double(0xff & version); + this.declareProperty(ussDouble); + return ussDouble; + } + + protected final USS2Byte declareByte(byte version) + { + var ussByte = new USS2Byte(0xff & version); + this.declareProperty(ussByte); + return ussByte; + } + + protected final USS2String declareString(byte version, byte length) + { + var ussByte = new USS2String(0xff & version, 0xff & length); + this.declareProperty(ussByte); + return ussByte; + } + + public static abstract class USS2Property + { + private final int version; + private final int objectSize; + protected final int[] offsets; + + protected USS2Property(int version, int objectSize) + { + this.version = version; + this.objectSize = objectSize; + this.offsets = new int[MAX_VERSION]; + } + + final int getObjectSize() + { + return this.objectSize; + } + + int getVersion() + { + return this.version; + } + } + + public static final class USS2Int extends USS2Property + { + private USS2Int(int version) + { + super(version, Integer.BYTES); + } + + public final void write(USS2PropertyObject po, int value) + { + po.dirty = true; + po.data.putInt(this.offsets[po.currentVersion], value); + } + + public final int read(USS2PropertyObject po) + { + if (this.offsets[po.currentVersion] == DOES_NOT_EXIST_IN_VERSION) + { + return 0; + } + + return po.data.getInt(this.offsets[po.currentVersion]); + } + } + + public static final class USS2Long extends USS2Property + { + private USS2Long(int version) + { + super(version, Long.BYTES); + } + + public final void write(USS2PropertyObject po, long value) + { + po.dirty = true; + po.data.putLong(this.offsets[po.currentVersion], value); + } + + public final long read(USS2PropertyObject po) + { + if (this.offsets[po.currentVersion] == DOES_NOT_EXIST_IN_VERSION) + { + return 0; + } + + return po.data.getLong(this.offsets[po.currentVersion]); + } + } + + public static final class USS2Double extends USS2Property + { + private USS2Double(int version) + { + super(version, Double.BYTES); + } + + public final void write(USS2PropertyObject po, double value) + { + po.dirty = true; + po.data.putDouble(this.offsets[po.currentVersion], value); + } + + public final double read(USS2PropertyObject po) + { + if (this.offsets[po.currentVersion] == DOES_NOT_EXIST_IN_VERSION) + { + return 0.0; + } + + return po.data.getDouble(this.offsets[po.currentVersion]); + } + } + + public static final class USS2Byte extends USS2Property + { + public USS2Byte(int version) + { + super(version, Byte.BYTES); + } + + public final void write(USS2PropertyObject po, byte value) + { + po.dirty = true; + po.data.put(this.offsets[po.currentVersion], value); + } + + public final byte read(USS2PropertyObject po) + { + if (this.offsets[po.currentVersion] == DOES_NOT_EXIST_IN_VERSION) + { + return 0; + } + + return po.data.get(this.offsets[po.currentVersion]); + } + } + + public static final class USS2String extends USS2Property + { + private USS2String(int version, int length) + { + super(version, Byte.BYTES + length); + } + + public final void write(USS2PropertyObject po, String value) + { + po.dirty = true; + var offset = this.offsets[po.currentVersion]; + var data = value.getBytes(StandardCharsets.UTF_8); + po.data.position(offset); + po.data.put((byte) data.length); + po.data.put(data); + po.data.rewind(); + } + + public final String read(USS2PropertyObject po) + { + if (this.offsets[po.currentVersion] == DOES_NOT_EXIST_IN_VERSION) + { + return ""; + } + + var offset = this.offsets[po.currentVersion]; + po.data.position(offset); + var length = po.data.get(); + var bytes = new byte[0xff & length]; + po.data.get(bytes); + po.data.rewind(); + + return new String(bytes, StandardCharsets.UTF_8); + } + } +} diff --git a/settings.gradle b/settings.gradle index da0336b..6f7e97a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ -include 'plutolib', +include 'plutouss2', + 'plutolib', 'plutostatic', 'plutotexturing', 'plutomesher',