diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ExternalShapeImporter.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ExternalShapeImporter.java
new file mode 100644
index 0000000..a96d419
--- /dev/null
+++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ExternalShapeImporter.java
@@ -0,0 +1,98 @@
+package io.github.applecommander.bastools.api.shapes;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import io.github.applecommander.bastools.api.utils.Converters;
+
+/**
+ * Allow the import of an external shape. Processing is very dependent on
+ * being invoked in the "correct" manner!
+ *
+ * Prototype code:
+ *
+ * ; Read in external shape table: configure first and then "import" processes file.
+ * .external characters
+ * type=bin
+ * shapes=1-96
+ * import=imperator.bin
+ *
+ */
+public class ExternalShapeImporter {
+ private ShapeTable destination;
+ private String firstShapeLabel;
+ private Function importer = this::importShapeTableFromBinary;
+ private IntStream intStream = null;
+
+ public ExternalShapeImporter(ShapeTable destination, String firstShapeLabel) {
+ this.destination = destination;
+ this.firstShapeLabel = firstShapeLabel;
+ }
+
+ public void process(String line) {
+ Objects.requireNonNull(line);
+ String[] parts = line.split("=");
+ if (parts.length != 2) {
+ throw new RuntimeException(String.format(".external fields require an assignment for '%s'", line));
+ }
+ switch (parts[0].toLowerCase()) {
+ case "type":
+ switch (parts[1].toLowerCase()) {
+ case "bin":
+ importer = this::importShapeTableFromBinary;
+ break;
+ case "src":
+ importer = this::importShapeTableFromSource;
+ break;
+ default:
+ throw new RuntimeException(String.format("Unknown import type specified: '%s'", line));
+ }
+ break;
+ case "shapes":
+ intStream = Converters.toIntStream(parts[1]);
+ break;
+ case "import":
+ ShapeTable temp = importer.apply(parts[1]);
+ // Shapes in Applesoft are 1 based but Java List object is 0 based...
+ intStream.map(n -> n-1).mapToObj(temp.shapes::get).forEach(this::importShape);
+ break;
+ default:
+ throw new RuntimeException(String.format("Unknown assignment '%s' for .external", line));
+ }
+ }
+
+ public ShapeTable importShapeTableFromBinary(String filename) {
+ // FIXME May need access to Configuration for these nested files?
+ try {
+ Objects.requireNonNull(intStream, ".external requires that 'shapes' is specified");
+ return ShapeTable.read(Paths.get(filename));
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ public ShapeTable importShapeTableFromSource(String filename) {
+ // FIXME May need access to Configuration for these nested files?
+ try {
+ Objects.requireNonNull(intStream, ".external requires that 'shapes' is specified");
+ return ShapeGenerator.generate(Paths.get(filename));
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ public void importShape(Shape shape) {
+ if (firstShapeLabel != null) {
+ VectorShape vshape = new VectorShape(firstShapeLabel);
+ vshape.vectors.addAll(shape.toVector().vectors);
+ destination.shapes.add(vshape);
+ firstShapeLabel = null;
+ } else {
+ destination.shapes.add(shape);
+ }
+ }
+}
diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java
index 23be689..13dd6d6 100644
--- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java
+++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java
@@ -1,5 +1,13 @@
package io.github.applecommander.bastools.api.shapes;
+/**
+ * Represents a single Applesoft shape. Note that the interface is mostly useful to get at the
+ * bitmap or vector shapes. This also implies that these implementations need to transform between
+ * eachother!
+ *
+ * @see BitmapShape
+ * @see VectorShape
+ */
public interface Shape {
/** Indicates if this shape is empty. */
public boolean isEmpty();
diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeGenerator.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeGenerator.java
index 42fe888..aeafdbd 100644
--- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeGenerator.java
+++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeGenerator.java
@@ -44,6 +44,10 @@ public class ShapeGenerator {
st.shapes.add(bitmapShape);
shapeConsumer = bitmapShape::appendBitmapRow;
break;
+ case ".external":
+ ExternalShapeImporter importer = new ExternalShapeImporter(st, label);
+ shapeConsumer = importer::process;
+ break;
default:
if (line.length() == 0) {
// do nothing
diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java
index 4e35332..20e36b1 100644
--- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java
+++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java
@@ -16,7 +16,12 @@ import java.util.stream.Collectors;
import io.github.applecommander.bastools.api.utils.Streams;
+/**
+ * Represents an Applesoft shape table. Note that this direct class is somewhat useless,
+ * except for the I/O routines. Access the individual shapes via the {@code #shapes} list.
+ */
public class ShapeTable {
+ /** Read an existing Applesoft shape table binary file. */
public static ShapeTable read(byte[] data) {
Objects.requireNonNull(data);
ShapeTable shapeTable = new ShapeTable();
diff --git a/api/src/main/java/io/github/applecommander/bastools/api/utils/Converters.java b/api/src/main/java/io/github/applecommander/bastools/api/utils/Converters.java
index 5ea2114..038919a 100644
--- a/api/src/main/java/io/github/applecommander/bastools/api/utils/Converters.java
+++ b/api/src/main/java/io/github/applecommander/bastools/api/utils/Converters.java
@@ -1,5 +1,8 @@
package io.github.applecommander.bastools.api.utils;
+import java.util.Arrays;
+import java.util.stream.IntStream;
+
public class Converters {
private Converters() { /* Prevent construction */ }
@@ -19,10 +22,39 @@ public class Converters {
}
}
+ /**
+ * Convert a string to a boolean value allowing for "true" or "yes" to evaluate to Boolean.TRUE.
+ */
public static Boolean toBoolean(String value) {
if (value == null) {
return null;
}
return "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
}
+
+
+ /**
+ * Supports entry of values in ranges or comma-separated lists and combinations thereof.
+ *
+ * - Range:
m-n
where m
+ * - Distinct values:
a,b,c,d
.
+ * - Single value:
x
+ * - Combination:
m-n;a,b,c,d;x
.
+ *
+ */
+ public static IntStream toIntStream(String values) {
+ IntStream stream = IntStream.empty();
+ for (String range : values.split(";")) {
+ if (range.contains("-")) {
+ String[] parts = range.split("-");
+ int low = Integer.parseInt(parts[0]);
+ int high = Integer.parseInt(parts[1]);
+ stream = IntStream.concat(stream, IntStream.rangeClosed(low, high));
+ } else {
+ stream = IntStream.concat(stream,
+ Arrays.asList(range.split(",")).stream().mapToInt(Integer::parseInt));
+ }
+ }
+ return stream;
+ }
}
diff --git a/api/src/test/java/io/github/applecommander/bastools/api/utils/ConverterTest.java b/api/src/test/java/io/github/applecommander/bastools/api/utils/ConverterTest.java
new file mode 100644
index 0000000..d5b364e
--- /dev/null
+++ b/api/src/test/java/io/github/applecommander/bastools/api/utils/ConverterTest.java
@@ -0,0 +1,45 @@
+package io.github.applecommander.bastools.api.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class ConverterTest {
+ @Test
+ public void testToInteger() {
+ assertEquals(0x1000, Converters.toInteger("0x1000").intValue());
+ assertEquals(0x1000, Converters.toInteger("$1000").intValue());
+ assertEquals(1000, Converters.toInteger("1000").intValue());
+ }
+
+ @Test
+ public void testToBoolean() {
+ assertTrue(Converters.toBoolean("true"));
+ assertTrue(Converters.toBoolean("True"));
+ assertTrue(Converters.toBoolean("YES"));
+ assertFalse(Converters.toBoolean("faLse"));
+ assertFalse(Converters.toBoolean("No"));
+ assertFalse(Converters.toBoolean("notreally"));
+ }
+
+ @Test
+ public void testToIntStream_Range() {
+ final int[] expected = { 4, 5, 6, 7, 8 };
+ assertArrayEquals(expected, Converters.toIntStream("4-8").toArray());
+ }
+
+ @Test
+ public void testToIntStream_List() {
+ final int[] expected314159 = { 3, 1, 4, 1, 5, 9 };
+ assertArrayEquals(expected314159, Converters.toIntStream("3,1,4,1,5,9").toArray());
+
+ final int[] expected7 = { 7 };
+ assertArrayEquals(expected7, Converters.toIntStream("7").toArray());
+ }
+
+ @Test
+ public void testToIntStream_Complex() {
+ final int[] expected = { 1, 5,6,7, 9, 2,3,4, 8 };
+ assertArrayEquals(expected, Converters.toIntStream("1;5-7;9;2-4;8").toArray());
+ }
+}
diff --git a/samples/imperator.bin b/samples/imperator.bin
new file mode 100644
index 0000000..7437a14
Binary files /dev/null and b/samples/imperator.bin differ
diff --git a/samples/ships.st b/samples/ships.st
index 365681e..81579ab 100644
--- a/samples/ships.st
+++ b/samples/ships.st
@@ -109,3 +109,9 @@
.xx
x*x
xx.
+
+; "]IMPERATOR" font from Beagle Bros. "Apple Mechanic"
+.external characters
+ type=bin
+ shapes=1-96
+ import=imperator.bin