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. + * + */ + 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