diff --git a/SanityCheck.jar b/SanityCheck.jar new file mode 100644 index 0000000000000000000000000000000000000000..bebdf1da3e6277f966e8fff78ea96d884fe27b0b Binary files /dev/null and b/SanityCheck.jar differ diff --git a/devTools/javaSanityCheck/excluded b/devTools/javaSanityCheck/excluded new file mode 100644 index 0000000000000000000000000000000000000000..28ff834dcbab00ca86653880aa605a6a64163fd2 --- /dev/null +++ b/devTools/javaSanityCheck/excluded @@ -0,0 +1,16 @@ +#Add files or folders to be excluded. +#empty lines will match on ALL paths, so if you need one do it like this: +# +src/art/ +src/gui/svgFilters.tw +# +#excluded to reduce false positives until better solution: +src/pregmod/basenationalitiesControls.tw +src/pregmod/editGenetics.tw +src/pregmod/widgets/bodySwapReaction.tw +src/uncategorized/costsBudget.tw +src/uncategorized/initRules.tw +src/uncategorized/slaveAssignmentsReport.tw +src/uncategorized/storyCaption.tw +src/cheats/ +src/pregmod/customizeSlaveTrade.tw diff --git a/devTools/javaSanityCheck/htmlTags b/devTools/javaSanityCheck/htmlTags new file mode 100644 index 0000000000000000000000000000000000000000..751b084ff5822f4162e3f79ea622b91f60c5472b --- /dev/null +++ b/devTools/javaSanityCheck/htmlTags @@ -0,0 +1,45 @@ +#Allowed HTML tags +#Effectively everything that is allowed in a these statements like this: +#<tag> or <tag ???> +#when ;1 is specified it expects a matching closing tag like this: </tag> +#Do not add Twine Tags here. +#Characters outside of ASCII scope are not supported. +# +#included to reduce false positives until better solution +!-- +http://www.gnu.org/licenses/ +#html tags +a;1 +b;1 +blockquote;1 +body;1 +br;0 +button;1 +caption;1 +center;1 +dd;1 +div;1 +dl;1 +dt;1 +h1;1 +h2;1 +h3;1 +h4;1 +hr;0 +html;1 +i;1 +img;1 +input;0 +li;1 +option;1 +script;1 +select;1 +span;1 +strong;1 +style;1 +table;1 +td;1 +th;1 +tr;1 +tt;1 +ul;1 diff --git a/devTools/javaSanityCheck/src/DisallowedTagException.java b/devTools/javaSanityCheck/src/DisallowedTagException.java new file mode 100644 index 0000000000000000000000000000000000000000..f4cc75e736b162192a5f18bbc55a10edb21eaa38 --- /dev/null +++ b/devTools/javaSanityCheck/src/DisallowedTagException.java @@ -0,0 +1,8 @@ +package org.arkerthan.sanityCheck; + +public class DisallowedTagException extends RuntimeException { + + public DisallowedTagException(String tag) { + super(tag); + } +} diff --git a/devTools/javaSanityCheck/src/Main.java b/devTools/javaSanityCheck/src/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..eea3a4ff611203b0025cd29ce1fe431447f80e1c --- /dev/null +++ b/devTools/javaSanityCheck/src/Main.java @@ -0,0 +1,319 @@ +package org.arkerthan.sanityCheck; + +import org.arkerthan.sanityCheck.element.AngleBracketElement; +import org.arkerthan.sanityCheck.element.CommentElement; +import org.arkerthan.sanityCheck.element.Element; +import org.arkerthan.sanityCheck.element.KnownElement; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class Main { + + public static TagSearchTree<Tag> htmlTags, twineTags; + private static String currentFile; + private static int currentLine, currentPosition; + private static Stack<Element> stack; + private static List<SyntaxError> errors = new LinkedList<>(); + private static String[] excluded; + + public static void main(String[] args) { + //setup + setupExclude(); + setupHtmlTags(); + setupTwineTags(); + Path workingDir = Paths.get("").toAbsolutePath(); + + //actual sanityCheck + runSanityCheckInDirectory(workingDir, new File("src/")); + + //handle errors + for (SyntaxError e : + errors) { + System.out.println(e.getError()); + } + } + + + /** + * Goes through the whole directory including subdirectories and runs + * sanityCheck() on all .tw files + * + * @param dir to be checked + */ + private static void runSanityCheckInDirectory(Path workingDir, File dir) { + //subdirectories are checked recursively + + try { + for (File file : dir.listFiles()) { + if (file.isFile()) { //run sanityCheck if file is a .tw file + String path = file.getAbsolutePath(); + if (path.endsWith(".tw")) { + sanityCheck(workingDir.relativize(file.toPath())); + } + } else if (file.isDirectory()) { + runSanityCheckInDirectory(workingDir, file.getAbsoluteFile()); + } + } + } catch (NullPointerException e) { + e.printStackTrace(); + System.err.println("Couldn't find directory " + currentFile); + System.exit(-1); + } + } + + /** + * Runs the sanity check for one file. Does not run if file is excluded. + * + * @param path file to be checked + */ + private static void sanityCheck(Path path) { + File file = path.toFile(); + + // replace this with a known encoding if possible + Charset encoding = Charset.defaultCharset(); + + if (!excluded(file.getPath())) { + try { + currentFile = file.getPath(); + currentLine = 1; + stack = new Stack<>(); + + //actually opening and reading the file + try (InputStream in = new FileInputStream(file); + Reader reader = new InputStreamReader(in, encoding); + // buffer for efficiency + Reader buffer = new BufferedReader(reader)) { + handleCharacters(buffer); + } + } catch (IOException e) { + System.err.println("Couldn't read " + file); + } + } + } + + /** + * sets up the alphabetical search tree for fast access of HTML tags later + */ + private static void setupHtmlTags() { + //load HTML tags into a list + List<Tag> TagsList = new LinkedList<>(); + try { + + Files.lines(new File("devTools/javaSanityCheck/htmlTags").toPath()).map(String::trim) + .filter(s -> !s.startsWith("#")) + .forEach(s -> TagsList.add(parseTag(s))); + } catch (IOException e) { + System.err.println("Couldn't read devTools/javaSanityCheck/htmlTags"); + } + + //turn List into alphabetical search tree + try { + htmlTags = new TagSearchTree(TagsList); + } catch (ArrayIndexOutOfBoundsException e) { + System.err.println("Illegal Character in devTools/javaSanityCheck/htmlTags"); + System.exit(-1); + } + } + + /** + * sets up the alphabetical search tree for fast access of twine tags later + */ + private static void setupTwineTags() { + //load twine tags into a list + List<Tag> TagsList = new LinkedList<>(); + try { + + Files.lines(new File("devTools/javaSanityCheck/twineTags").toPath()).map(String::trim) + .filter(s -> !s.startsWith("#")) + .forEach(s -> TagsList.add(parseTag(s))); + } catch (IOException e) { + System.err.println("Couldn't read devTools/javaSanityCheck/twineTags"); + } + + //turn List into alphabetical search tree + try { + twineTags = new TagSearchTree(TagsList); + } catch (ArrayIndexOutOfBoundsException e) { + System.err.println("Illegal Character in devTools/javaSanityCheck/twineTags"); + System.exit(-1); + } + } + + /** + * Turns a string into a Tag + * ";1" at the end of the String indicates that the tag needs to be closed later + * + * @param s tag as String + * @return tag as Tag + */ + private static Tag parseTag(String s) { + String[] st = s.split(";"); + if (st.length > 1 && st[1].equals("1")) { + return new Tag(st[0], false); + } + return new Tag(st[0], true); + } + + /** + * sets up the excluded array. + */ + private static void setupExclude() { + //load excluded files + List<String> excludedList = new ArrayList<>(); + try { + Files.lines(new File("devTools/javaSanityCheck/excluded").toPath()).map(String::trim) + .filter(s -> !s.startsWith("#")) + .forEach(excludedList::add); + } catch (IOException e) { + System.err.println("Couldn't read devTools/javaSanityCheck/excluded"); + } + + //turn excluded files into an array and change them to windows style if needed + if (isWindows()) { + excluded = new String[excludedList.size()]; + int i = 0; + for (String s : + excludedList) { + excluded[i++] = s.replaceAll("/", "\\\\"); + } + } else { + excluded = excludedList.toArray(new String[0]); + } + } + + /** + * @return whether OS is Windows or not + */ + private static boolean isWindows() { + return (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")); + } + + /** + * checks if a file or directory is excluded from the sanity check + * + * @param s file/directory to be checked + * @return whether it is excluded or not + */ + private static boolean excluded(String s) { + for (String ex : + excluded) { + if (s.startsWith(ex)) return true; + } + return false; + } + + /** + * Reads the file character by character. + * + * @param reader reader that is read + * @throws IOException thrown if the file can't be read + */ + private static void handleCharacters(Reader reader) throws IOException { + int r; + while ((r = reader.read()) != -1) { + char c = (char) r; + handleCharacter(c); + } + } + + /** + * Handles a single character + * + * @param c next character + */ + private static void handleCharacter(char c) { + //updating position + currentPosition++; + if (c == '\n') { + currentLine++; + currentPosition = 1; + } + + //try applying to the innermost element + if (!stack.empty()) { + int change; + try { + change = stack.peek().handleChar(c); + } catch (SyntaxError e) { + change = e.getChange(); + addError(e); + } + + //change greater 0 means the innermost element did some work + if (change > 0) { + //2 means the Element is complete + if (change == 2) { + //remove the topmost element from stack since it is complete + stack.pop(); + return; + } + //3 means the Element is complete and part of a two tag system + if (change == 3) { + //remove the topmost element from stack since it is complete + KnownElement k = stack.pop().getKnownElement(); + /*if (k.isOpening()) { + stack.push(k); + } else */ + if (k.isClosing()) { + if (stack.empty()) { + addError(new SyntaxError("Closed tag " + k.getShortDescription() + " without having any open tags.", -2)); + } else if (stack.peek() instanceof KnownElement) { + KnownElement kFirst = (KnownElement) stack.pop(); + if (!kFirst.isMatchingElement(k)) { + addError(new SyntaxError("Opening tag " + kFirst.getShortDescription() + + " does not match closing tag " + k.getShortDescription() + ".", -2)); + } + //stack.pop(); + } else { + addError(new SyntaxError("Closing tag " + k.getShortDescription() + " inside " + + "another tag: " + stack.peek().getShortDescription(), -2, true)); + } + } + if (k.isOpening()) { + stack.push(k); + } + return; + } + if (change == 4) { + stack.pop(); + } else { + return; + } + } + } + + + //innermost element was uninterested, trying to find matching element + switch (c) { + //case '@': + // stack.push(new AtElement(currentLine, currentPosition)); + //break; + case '<': + stack.push(new AngleBracketElement(currentLine, currentPosition)); + break; + //case '>': + //addError(new SyntaxError("Dangling \">\", current innermost: " + (stack.empty() ? "null" : stack.peek().getShortDescription()), -2)); + //break; + case '/': + stack.push(new CommentElement(currentLine, currentPosition)); + break; + } + } + + /** + * add an error to the error list + * + * @param e new error + */ + private static void addError(SyntaxError e) { + e.setFile(currentFile); + e.setLine(currentLine); + e.setPosition(currentPosition); + errors.add(e); + } +} diff --git a/devTools/javaSanityCheck/src/SyntaxError.java b/devTools/javaSanityCheck/src/SyntaxError.java new file mode 100644 index 0000000000000000000000000000000000000000..150f2bd449b8c318b5a05884868a71538554ec67 --- /dev/null +++ b/devTools/javaSanityCheck/src/SyntaxError.java @@ -0,0 +1,40 @@ +package org.arkerthan.sanityCheck; + +public class SyntaxError extends Exception { + private String file; + private int line, position; + private String description; + private int change; //see Element for values; -2 means not thrown + private boolean warning = false; + + public SyntaxError(String description, int change) { + this.description = description; + this.change = change; + } + + public SyntaxError(String description, int change, boolean warning) { + this(description, change); + this.warning = warning; + } + + public void setFile(String file) { + this.file = file; + } + + public void setLine(int line) { + this.line = line; + } + + public void setPosition(int position) { + this.position = position; + } + + public String getError() { + String s = warning ? "Warning: " : "Error: "; + return s + file + ": " + line + ":" + position + " : "+ description; + } + + public int getChange() { + return change; + } +} diff --git a/devTools/javaSanityCheck/src/Tag.java b/devTools/javaSanityCheck/src/Tag.java new file mode 100644 index 0000000000000000000000000000000000000000..8a13ee4f7a178399db3eaf626b693e8a12ae71df --- /dev/null +++ b/devTools/javaSanityCheck/src/Tag.java @@ -0,0 +1,11 @@ +package org.arkerthan.sanityCheck; + +public class Tag { + public final String tag; + public final boolean single; + + public Tag(String tag, boolean single) { + this.tag = tag; + this.single = single; + } +} diff --git a/devTools/javaSanityCheck/src/TagSearchTree.java b/devTools/javaSanityCheck/src/TagSearchTree.java new file mode 100644 index 0000000000000000000000000000000000000000..dde49df879a346f124d15647cfeb821d0e0735fb --- /dev/null +++ b/devTools/javaSanityCheck/src/TagSearchTree.java @@ -0,0 +1,80 @@ +package org.arkerthan.sanityCheck; + +import java.util.List; + +/** + * Tag SearchTree stores Tags in an alphabetical search tree. + * Once created the search tree can't be changed anymore. + * + * @param <E> Tag class to be stored + */ +public class TagSearchTree<E extends Tag> { + private static final int SIZE = 128; + private final TagSearchTree<E>[] branches; + private E element = null; + private String path; + + /** + * creates a new empty TagSearchTree + */ + private TagSearchTree() { + branches = new TagSearchTree[SIZE]; + } + + /** + * Creates a new filled TagSearchTree + * + * @param list Tags to be inserted + */ + public TagSearchTree(List<E> list) { + this(); + for (E e : list) { + this.add(e, 0); + } + } + + /** + * adds a new Tag to the TagSearchTree + * + * @param e Tag to be stored + * @param index index of relevant char for adding in tag + */ + private void add(E e, int index) { + //set the path to here + path = e.tag.substring(0, index); + //checks if tag has to be stored here or further down + if (e.tag.length() == index) { + element = e; + } else { + //store tag in correct branch + char c = e.tag.charAt(index); + if (branches[c] == null) { + branches[c] = new TagSearchTree<>(); + } + branches[c].add(e, index + 1); + } + } + + /** + * @param c character of branch needed + * @return branch or null if branch doesn't exist + */ + public TagSearchTree<E> getBranch(char c) { + if (c >= SIZE) return null; + return branches[c]; + } + + /** + * @return stored Tag, null if empty + */ + public E getElement() { + return element; + } + + /** + * @return path inside full tree to get to this Branch + */ + public String getPath() { + return path; + } +} diff --git a/devTools/javaSanityCheck/src/UnknownStateException.java b/devTools/javaSanityCheck/src/UnknownStateException.java new file mode 100644 index 0000000000000000000000000000000000000000..dceb35bc3d5b57fa3d710e9e61d3ce427a3499c3 --- /dev/null +++ b/devTools/javaSanityCheck/src/UnknownStateException.java @@ -0,0 +1,8 @@ +package org.arkerthan.sanityCheck; + +public class UnknownStateException extends RuntimeException { + + public UnknownStateException(int state) { + super(String.valueOf(state)); + } +} diff --git a/devTools/javaSanityCheck/src/element/AngleBracketElement.java b/devTools/javaSanityCheck/src/element/AngleBracketElement.java new file mode 100644 index 0000000000000000000000000000000000000000..e4819ba37ea48e4924dc944827d651fa14e02ad1 --- /dev/null +++ b/devTools/javaSanityCheck/src/element/AngleBracketElement.java @@ -0,0 +1,360 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.*; + +import java.util.Arrays; +import java.util.List; + +public class AngleBracketElement extends Element { + private static final List<String> logicTags = Arrays.asList("if", "elseif", "else", "switch", "case", "default"); + private int state = 0; + /* + 0 - initial: < + TWINE + 1 - << + -1 - <</ + 2 - trying to complete twine tag: <<tag ???>> + -2 - trying to complete twine tag: <</tag>> + 3 - waiting for >> + -3 - expecting > from 3 + 4 - waiting for >> with KnownElement + -4 - expecting > from 4 + 5 - expecting >> + -5 - expecting > + 6 - expecting > with KnownElement opening; comparison? + -6 - expecting > with KnownElement closing + + HTML + -9 - </ + 10 - trying to complete HTML tag: <tag ???> + -10 - trying to complete HTML tag: </tag> + 11 - waiting for > + -11 - expecting > + 12 - waiting for > with KnownElement + */ + + private TagSearchTree<Tag> tree; + + public AngleBracketElement(int line, int pos) { + super(line, pos); + } + + @Override + public int handleChar(char c) throws SyntaxError { + switch (state) { + case 0: + switch (c) { + case '<': + state = 1; + return 1; + case '>': + throw new SyntaxError("Empty Statement?", 2); + case '/': + state = -9; + return 1; + case ' ':// assume comparison + case '=':// " + return 2; + case '3'://a heart: <3 + return 2; + default: + try { + state = 10; + tree = Main.htmlTags; + return handleOpeningHTML(c); + } catch (SyntaxError e) { + state = 1; + throw new SyntaxError("Opening \"<\" missing, found " + c + " [debug:initialCase]", 1); + } + } + case 1: + if (c == '<') { + throw new SyntaxError("Too many \"<\".", 1); + } else if (c == '>') { + state = 3; + throw new SyntaxError("Empty Statement?", 1); + } else if (c == '/') { + state = -1; + return 1; + } + state = 2; + tree = Main.twineTags; + return handleOpeningTwine(c); + case -1: + if (c == '>') { + throw new SyntaxError("Empty Statement?", 2, true); + } + state = -2; + tree = Main.twineTags; + return handleClosingTwine(c); + + case 2: + return handleOpeningTwine(c); + case -2: + return handleClosingTwine(c); + case 3: + if (c == '>') { + state = -3; + return 1; + } + break; + case -3: + if (c == '>') { + return 2; + } else if (c == ' ' || c == '=') { // assuming comparison + state = 3; + return 1; + } else { + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 2); + } + case 4: + if (c == '>') { + state = -4; + return 1; + } + break; + case -4: + if (c == '>') { + return 3; + } else if (c == ' ' || c == '=') { // assuming comparison + state = 4; + return 1; + } else { + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 2); + } + case 5: + if (c == '>') { + state = -5; + return 1; + } else { + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 2); + } + case -5: + if (c == '>') { + return 2; + } + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 2); + case 6: + if (c == '>') { + return 3; + } else if (c == ' ' || c == '=') { + state = 3; + return 1; + } else { + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 3); + } + case -6: + if (c == '>') { + return 3; + } + throw new SyntaxError("Closing \">\" missing, opened [" + line + ":" + pos + "]", 3); + + case -9: + if (c == '>') { + throw new SyntaxError("Empty Statement?", 2, true); + } + state = -10; + tree = Main.htmlTags; + return handleClosingHTML(c); + case 10: + return handleOpeningHTML(c); + case -10: + return handleClosingHTML(c); + case 11: + if (c == '>') + return 2; + if (c == '@') //@ inside HTML tags is allowed + return 1; + break; + case -11: + if (c == '>') + return 2; + throw new SyntaxError("Closing \">\" missing [2]", 2); + case 12: + if (c == '>') + return 3; + if (c == '@') //@ inside HTML tags is allowed + return 1; + break; + default: + throw new UnknownStateException(state); + } + return 0; + } + + private int handleOpeningHTML(char c) throws SyntaxError { + if (c == ' ') { + state = 11; + if (tree.getElement() == null) { + throw new SyntaxError("Unknown HTML tag", 1); + } + if (!tree.getElement().single) { + k = new KnownHtmlElement(line, pos, true, tree.getElement().tag); + state = 12; + return 1; + } + return 1; + } + if (c == '>') { + if (tree.getElement() == null) { + throw new SyntaxError("Unknown HTML tag", 2); + } + if (!tree.getElement().single) { + k = new KnownHtmlElement(line, pos, true, tree.getElement().tag); + return 3; + } + return 2; + } + + tree = tree.getBranch(c); + if (tree == null) { + state = 11; + throw new SyntaxError("Unknown HTML tag or closing \">\" missing, found " + c, 1); + } + + return 1; + } + + private int handleClosingHTML(char c) throws SyntaxError { + if (c == '>') { + if (tree.getElement() == null) { + throw new SyntaxError("Unknown HTML tag", 2); + } + if (tree.getElement().single) { + throw new SyntaxError("Single HTML tag used as closing Tag: " + tree.getElement().tag, 2); + } + k = new KnownHtmlElement(line, pos, false, tree.getElement().tag); + return 3; + } + + tree = tree.getBranch(c); + if (tree == null) { + state = -11; + throw new SyntaxError("Unknown HTML tag or closing \">\" missing, found " + c, 1); + } + + return 1; + } + + + private int handleOpeningTwine(char c) throws SyntaxError { + if (c == ' ') { + state = 3; + if (tree.getElement() == null) { + //assuming not listed means widget until better solution + return 1; + //throw new SyntaxError("Unknown Twine tag or closing \">>\" missing, found " + tree.getPath(), 1); + } + if (!tree.getElement().single) { + if (logicTags.contains(tree.getElement().tag)) { + k = new KnownLogicElement(line, pos, tree.getElement().tag, false); + } else { + k = new KnownTwineElement(line, pos, true, tree.getElement().tag); + } + state = 4; + return 1; + } + return 1; + } + if (c == '>') { + state = -5; + if (tree.getElement() == null) { + //assuming not listed means widget until better solution + //throw new SyntaxError("Unknown Twine tag or closing \">>\" missing, found " + tree.getPath(), 1); + return 1; + } + if (!tree.getElement().single) { + if (logicTags.contains(tree.getElement().tag)) { + k = new KnownLogicElement(line, pos, tree.getElement().tag, false); + } else { + k = new KnownTwineElement(line, pos, true, tree.getElement().tag); + } + state = 6; + return 1; + } + return 2; + } + + tree = tree.getBranch(c); + if (tree == null) { + //assuming not listed means widget until better solution + state = 3; + //throw new SyntaxError("Unknown Twine tag or closing \">>\" missing, found " + c, 1); + } + + return 1; + } + + private int handleClosingTwine(char c) throws SyntaxError { + if (c == '>') { + if (tree.getElement() == null) { + throw new SyntaxError("Unknown Twine tag", 2); + } + if (tree.getElement().single) { + throw new SyntaxError("Single Twine tag used as closing Tag: " + tree.getElement().tag, 2); + } + if (logicTags.contains(tree.getElement().tag)) { + k = new KnownLogicElement(line, pos, tree.getElement().tag, true); + } else { + k = new KnownTwineElement(line, pos, false, tree.getElement().tag); + } + state = -6; + return 1; + } + + tree = tree.getBranch(c); + if (tree == null) { + state = 3; + throw new SyntaxError("Unknown Twine closing tag or closing \">>\" missing, found " + c, 1); + } + + return 1; + } + + @Override + public String getShortDescription() { + StringBuilder builder = new StringBuilder(); + builder.append('[').append(line).append(":").append(pos).append("] "); + switch (state) { + case 0: + builder.append("<"); + break; + case 1: + builder.append("<<"); + break; + case -1: + builder.append("<</"); + break; + case 2: + builder.append("<<").append(tree.getPath()); + break; + case -2: + builder.append("<</").append(tree.getPath()); + break; + case 3: + builder.append("<<??? ???"); + break; + case 4: + builder.append("<<?").append(tree.getPath()).append(" ???"); + break; + case -3: + builder.append("<<??? ???>"); + break; + case -4: + builder.append("<<?").append(tree.getPath()).append(" ???>"); + break; + case 5: + builder.append("<").append(tree.getPath()).append(" ???"); + break; + case -5: + builder.append("</").append(tree.getPath()); + break; + case 6: + builder.append("<").append(tree.getPath()).append(" ???"); + break; + default: + //throw new UnknownStateException(state); + } + return builder.toString(); + } +} diff --git a/devTools/javaSanityCheck/src/element/AtElement.java b/devTools/javaSanityCheck/src/element/AtElement.java new file mode 100644 index 0000000000000000000000000000000000000000..0dad04247c50f2a9a77bb9ce584ded8de0377ed2 --- /dev/null +++ b/devTools/javaSanityCheck/src/element/AtElement.java @@ -0,0 +1,105 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.SyntaxError; +import org.arkerthan.sanityCheck.UnknownStateException; + +public class AtElement extends Element { + private int state = 0; + // 0 = @ + // 1 = @@ + // 2 = @@. + // 3 = @@.a -- @@.ab -- @@.abc + // 4 = @@.abc;abc + // 5 = @@.abc;abc@ + + // example: @@.red;some text@@ + + public AtElement(int line, int pos) { + super(line, pos); + } + + @Override + public int handleChar(char c) throws SyntaxError { + switch (state) { + case 0: + state = 1; + if (c == '@') { + return 1; + } else { + if (c == '.') { + state = 2; + } + throw new SyntaxError("Opening \"@\" missing.", 1); + } + case 1: + if (c == '.') { + state = 2; + return 1; + } else { + state = 4; + throw new SyntaxError("\".\" missing, found \"" + c + "\". This might also indicate a " + + "missing closure in the previous color code.", 0, true); + } + case 2: + state = 3; + if (Character.isAlphabetic(c)) { + return 1; + } else { + throw new SyntaxError("Identifier might be wrong.", 1, true); + } + case 3: + if (c == ';') { + state = 4; + return 1; + } else if (c == ' ') { + state = 4; + throw new SyntaxError("\";\" missing or wrong space.", 1); + } + break; + case 4: + if (c == '@') { + state = 5; + return 1; + } + break; + case 5: + if (c == '@') { + return 2; + } else { + throw new SyntaxError("Closing \"@\" missing.", 2); + } + default: + throw new UnknownStateException(state); + } + return 0; + } + + @Override + public String getShortDescription() { + StringBuilder builder = new StringBuilder(); + builder.append(line).append(":").append(pos).append(" "); + switch (state) { + case 0: + builder.append("@"); + break; + case 1: + builder.append("@@"); + break; + case 2: + builder.append("@@."); + break; + case 3: + builder.append("@@.???"); + break; + case 4: + builder.append("@@???"); + break; + case 5: + builder.append("@@???@"); + break; + default: + throw new UnknownStateException(state); + } + return builder.toString(); + } +} diff --git a/devTools/javaSanityCheck/src/element/CommentElement.java b/devTools/javaSanityCheck/src/element/CommentElement.java new file mode 100644 index 0000000000000000000000000000000000000000..3a3cc12aff9f3c2e4b06cc7240d14824575cc96e --- /dev/null +++ b/devTools/javaSanityCheck/src/element/CommentElement.java @@ -0,0 +1,66 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.SyntaxError; +import org.arkerthan.sanityCheck.UnknownStateException; + +public class CommentElement extends Element { + int state = 0; + /* + 0 - / + 1 - /*??? + 2 - /*???* + 3 - /%??? + 4 - /%???% + */ + + public CommentElement(int line, int pos) { + super(line, pos); + } + + @Override + public int handleChar(char c) throws SyntaxError { + switch (state) { + case 0: + if (c == '*') { + state = 1; + } else if (c == '%') { + state = 3; + } else if (c == '>') { + throw new SyntaxError("XHTML style closure", 4, true); + } else { + return 4; + } + break; + case 1: + if (c == '*') { + state = 2; + } + break; + case 2: + if (c == '/') { + return 2; + } + state = 1; + break; + case 3: + if (c == '%') { + state = 4; + } + break; + case 4: + if (c == '/') { + return 2; + } + state = 3; + break; + default: + throw new UnknownStateException(state); + } + return 1; + } + + @Override + public String getShortDescription() { + return null; + } +} diff --git a/devTools/javaSanityCheck/src/element/Element.java b/devTools/javaSanityCheck/src/element/Element.java new file mode 100644 index 0000000000000000000000000000000000000000..b0f99fa904f66a6a9ebcecb5ffdfb7040493b08b --- /dev/null +++ b/devTools/javaSanityCheck/src/element/Element.java @@ -0,0 +1,40 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.SyntaxError; + +public abstract class Element { + protected KnownElement k; + protected int line, pos; + + /** + * @param line Line the instance was created + * @param pos Position in line the instance was created + */ + protected Element(int line, int pos) { + this.line = line; + this.pos = pos; + } + + /** + * Parses a Char and returns an int depending on the state of the element + * 0 - the Element did nothing + * 1 - the Element changed state + * 2 - the Element is finished + * 3 - the Element is finished and a KnownHtmlElement was generated + * 4 - the Element is finished and the char is still open for use + * + * @param c + * @return + * @throws Error + */ + public abstract int handleChar(char c) throws SyntaxError; + + public KnownElement getKnownElement() { + return k; + } + + /** + * @return a short description usually based on state and position of the Element + */ + public abstract String getShortDescription(); +} diff --git a/devTools/javaSanityCheck/src/element/KnownElement.java b/devTools/javaSanityCheck/src/element/KnownElement.java new file mode 100644 index 0000000000000000000000000000000000000000..305be2fe65210b191d96ead352487b35e448ce39 --- /dev/null +++ b/devTools/javaSanityCheck/src/element/KnownElement.java @@ -0,0 +1,31 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.SyntaxError; + +public abstract class KnownElement extends Element { + + public KnownElement(int line, int pos) { + super(line, pos); + } + + /** + * @return true, if it needs another Known Element to close it. + */ + public abstract boolean isOpening(); + + /** + * @return true if it closes another Element. + */ + public abstract boolean isClosing(); + + /** + * @param k Element to be checked + * @return true if given Element closes Element + */ + public abstract boolean isMatchingElement(KnownElement k); + + @Override + public int handleChar(char c) throws SyntaxError { + return 0; + } +} diff --git a/devTools/javaSanityCheck/src/element/KnownHtmlElement.java b/devTools/javaSanityCheck/src/element/KnownHtmlElement.java new file mode 100644 index 0000000000000000000000000000000000000000..d086a74bc547a0aa90520e50c4398f5cf6dc365e --- /dev/null +++ b/devTools/javaSanityCheck/src/element/KnownHtmlElement.java @@ -0,0 +1,41 @@ +package org.arkerthan.sanityCheck.element; + +public class KnownHtmlElement extends KnownElement { + + private boolean opening; + private String statement; + + public KnownHtmlElement(int line, int pos, boolean opening, String statement) { + super(line, pos); + this.opening = opening; + this.statement = statement; + } + + @Override + public String getShortDescription() { + StringBuilder builder = new StringBuilder(); + builder.append('[').append(line).append(":").append(pos).append("] <"); + if (!opening) { + builder.append("/"); + } + return builder.append(statement).append(">").toString(); + } + + @Override + public boolean isOpening() { + return opening; + } + + @Override + public boolean isClosing() { + return !opening; + } + + @Override + public boolean isMatchingElement(KnownElement k) { + if (k instanceof KnownHtmlElement) { + return ((KnownHtmlElement) k).statement.equals(this.statement); + } + return false; + } +} diff --git a/devTools/javaSanityCheck/src/element/KnownLogicElement.java b/devTools/javaSanityCheck/src/element/KnownLogicElement.java new file mode 100644 index 0000000000000000000000000000000000000000..502abde93a296c3dffb2391b75b08893aeecd2c5 --- /dev/null +++ b/devTools/javaSanityCheck/src/element/KnownLogicElement.java @@ -0,0 +1,117 @@ +package org.arkerthan.sanityCheck.element; + +import org.arkerthan.sanityCheck.DisallowedTagException; +import org.arkerthan.sanityCheck.UnknownStateException; + +import java.util.Arrays; +import java.util.List; + +public class KnownLogicElement extends KnownElement { + private static final List<String> allowedTags = Arrays.asList("if", "elseif", "else"); + private final int state; + private boolean last; + /* + 0 - if + 1 - elseif + 2 - else + 3 - switch + 4 - case + 5 - default + */ + + public KnownLogicElement(int line, int pos, String tag, boolean last) { + this(line, pos, tag); + this.last = last; + } + + public KnownLogicElement(int line, int pos, String tag) { + super(line, pos); + switch (tag) { + case "if": + state = 0; + break; + case "elseif": + state = 1; + break; + case "else": + state = 2; + break; + case "switch": + state = 3; + break; + case "case": + state = 4; + break; + case "default": + state = 5; + break; + default: + throw new DisallowedTagException(tag); + } + last = false; + } + + @Override + public boolean isOpening() { + return !last; + } + + @Override + public boolean isClosing() { + return (state != 0 && state != 3) || last; + } + + @Override + public boolean isMatchingElement(KnownElement k) { + if (!(k instanceof KnownLogicElement)) { + return false; + } + KnownLogicElement l = (KnownLogicElement) k; + switch (state) { + case 0: + case 1: + return l.state == 1 || l.state == 2 || (l.state == 0 && l.last); + case 2: + return l.state == 0 && l.last; + case 3: + case 4: + return l.state == 3 || l.state == 4; + case 5: + return l.state == 3 && l.last; + default: + throw new UnknownStateException(state); + } + } + + @Override + public String getShortDescription() { + StringBuilder builder = new StringBuilder(); + builder.append("[").append(line).append(":").append(pos).append("] <<"); + if (last) { + builder.append('/'); + } + switch (state) { + case 0: + builder.append("if"); + break; + case 1: + builder.append("elseif"); + break; + case 2: + builder.append("else"); + break; + case 3: + builder.append("switch"); + break; + case 4: + builder.append("case"); + break; + case 5: + builder.append("default"); + break; + default: + throw new UnknownStateException(state); + } + return builder.append(">>").toString(); + } +} diff --git a/devTools/javaSanityCheck/src/element/KnownTwineElement.java b/devTools/javaSanityCheck/src/element/KnownTwineElement.java new file mode 100644 index 0000000000000000000000000000000000000000..24003fd00be0bb79aa6a01b2fbd2d9a91439ac6a --- /dev/null +++ b/devTools/javaSanityCheck/src/element/KnownTwineElement.java @@ -0,0 +1,41 @@ +package org.arkerthan.sanityCheck.element; + +public class KnownTwineElement extends KnownElement { + + private boolean opening; + private String statement; + + public KnownTwineElement(int line, int pos, boolean opening, String statement) { + super(line, pos); + this.opening = opening; + this.statement = statement; + } + + @Override + public String getShortDescription() { + StringBuilder builder = new StringBuilder(); + builder.append("[").append(line).append(":").append(pos).append("] <<"); + if (!opening) { + builder.append("/"); + } + return builder.append(statement).append(">>").toString(); + } + + @Override + public boolean isOpening() { + return opening; + } + + @Override + public boolean isClosing() { + return !opening; + } + + @Override + public boolean isMatchingElement(KnownElement k) { + if (k instanceof KnownTwineElement) { + return ((KnownTwineElement) k).statement.equals(this.statement); + } + return false; + } +} diff --git a/devTools/javaSanityCheck/twineTags b/devTools/javaSanityCheck/twineTags new file mode 100644 index 0000000000000000000000000000000000000000..e1dea71621bd818041d6aad43d7b3053b2c78132 --- /dev/null +++ b/devTools/javaSanityCheck/twineTags @@ -0,0 +1,39 @@ +#Allowed twine tags +#Effectively everything that is allowed in a statements like this: +#<<tag>> or <<tag ???>> +#when ;1 is specified it expects a matching closing tag like this: <</tag>> +#Everything belonging to if and switch statements is hard coded and does not +#need to be included here. +#Do not add HTML Tags here. +#Characters outside of ASCII scope are not supported. +# +#twine tags +capture;1 +continue;0 +for;1 +#does foreach really exist? +foreach;1 +goto;0 +htag;1 +include;0 +link;1 +nobr;1 +print;0 +replace;1 +run;0 +script;1 +set;0 +silently;1 +textbox;0 +timed;1 +unset;0 +widget;1 +=;0 +# +# Twine logic ### DO NOT TOUCH ### +if;1 +elseif;1 +else;1 +switch;1 +case;1 +default; diff --git a/sanityCheck-java b/sanityCheck-java new file mode 100755 index 0000000000000000000000000000000000000000..455bbfbc243ec664f5aac2daa2ad74903d104962 --- /dev/null +++ b/sanityCheck-java @@ -0,0 +1 @@ +java -jar SanityCheck.jar diff --git a/sanityCheck-java.bat b/sanityCheck-java.bat new file mode 100644 index 0000000000000000000000000000000000000000..455bbfbc243ec664f5aac2daa2ad74903d104962 --- /dev/null +++ b/sanityCheck-java.bat @@ -0,0 +1 @@ +java -jar SanityCheck.jar diff --git a/src/events/intro/introSummary.tw b/src/events/intro/introSummary.tw index a91ccd914ba27167096a8773aceab38be01fde46..afcb75c701b1d1a4e1e2482a37af243c95366380 100644 --- a/src/events/intro/introSummary.tw +++ b/src/events/intro/introSummary.tw @@ -112,7 +112,7 @@ You are using standardized slave trading channels. [[Customize the slave trade|C <<if ndef $nationalitiescheck>> /* NGP: regenerate $nationalitiescheck from previous game's $nationalities array */ <<silently>><<include "Customize Slave Trade">><</silently>> <</if>> - <br style="clear:both" /><hr style="margin:0"> + <br style="clear:both"><hr style="margin:0"> <<set _len = Object.keys($nationalitiescheck).length>> <<set _j = 0>> <<for _nation, _i range $nationalitiescheck>> @@ -120,7 +120,7 @@ You are using standardized slave trading channels. [[Customize the slave trade|C <<set _j++>> <<if _j < _len>> | <</if>> <</for>> - <br style="clear:both" /><hr style="margin:0"> + <br style="clear:both"><hr style="margin:0"> <</if>> /* closes $customVariety is defined */ /* Accordion 000-250-006 */ diff --git a/src/facilities/farmyard/farmyard.tw b/src/facilities/farmyard/farmyard.tw index 84c46326557d1a0e48a88b846e0610fedf7e9e83..b44ac1f37449d5c9c73970c43d2728e1ae0fcbd5 100644 --- a/src/facilities/farmyard/farmyard.tw +++ b/src/facilities/farmyard/farmyard.tw @@ -105,7 +105,7 @@ $farmyardNameCaps is an oasis of growth in the midst of the jungle of steel and <br>It can support $farmyard farmhands. Currently there <<if $farmyardSlaves == 1>>is<<else>>are<</if>> $farmyardSlaves farmhand<<if $farmyardSlaves != 1>>s<</if>> at $farmyardName. [[Expand the farmyard|Farmyard][cashX(forceNeg(_Tmult0), "capEx"), $farmyard += 5, $PC.engineering += .1]] //Costs <<print cashFormat(_Tmult0)>> and will increase upkeep costs// -<span id="menials" +<span id="menials"> <br><br><br> <<if $farmMenials > 0>> Assigned here are $farmMenials slaves working to produce as much food as possible. diff --git a/src/facilities/nursery/childSummary.tw b/src/facilities/nursery/childSummary.tw index 760106b9eeeed54014ee3ca43e64014d387b4169..78449006c71ef6d6c5753beb58d866e9c3b713cb 100644 --- a/src/facilities/nursery/childSummary.tw +++ b/src/facilities/nursery/childSummary.tw @@ -31,7 +31,7 @@ <<if (/Select/i.test(_Pass))>> <<set _offset = -25>> <</if>> - <br /> + <br> <<set _tableCount = _tableCount || 0>> <<set _tableCount++>> /* @@ -104,7 +104,7 @@ <<set _chosenClothes = saChoosesOwnClothes(_Child)>> <<set $cribs[_csi].devotion = _oldDevotion, _Child = $cribs[_csi]>> /* restore devotion value so repeatedly changing clothes isn't an exploit */ <</if>> - <br style="clear:both" /><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>><<if ($seeImages == 1) && ($seeSummaryImages == 1)>><div class="imageRef smlImg"><<SlaveArt _Child 1>></div><</if>> + <br style="clear:both"><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>><<if ($seeImages == 1) && ($seeSummaryImages == 1)>><div class="imageRef smlImg"><<SlaveArt _Child 1>></div><</if>> <<if "be your Head Girl" == _Child.assignment>>''@@.lightcoral;HG@@'' <<elseif "recruit girls" == _Child.assignment>>''@@.lightcoral;RC@@'' <<elseif "guard you" == _Child.assignment>>''@@.lightcoral;BG@@'' @@ -114,7 +114,7 @@ /* TODO: will the PC be able to give children PA? */ <<case "Personal Attention Select">> - <br style="clear:both" /><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>><<if ($seeImages == 1) && ($seeSummaryImages == 1)>><div class="imageRef smlImg"><<SlaveArt _Child 1>></div><</if>> + <br style="clear:both"><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>><<if ($seeImages == 1) && ($seeSummaryImages == 1)>><div class="imageRef smlImg"><<SlaveArt _Child 1>></div><</if>> <<link _childName>> <<if !Array.isArray($personalAttention)>> /* first PA target */ <<set $personalAttention = [{ID: $cribs[_csi].ID, trainingRegimen: "undecided"}]>> diff --git a/src/uncategorized/RESS.tw b/src/uncategorized/RESS.tw index 40c313b72694a50904f2f50fb66a54938c0fa806..148413175ddb56d90efdfe5a1e2160c41695cc73 100644 --- a/src/uncategorized/RESS.tw +++ b/src/uncategorized/RESS.tw @@ -18470,7 +18470,7 @@ You tell $him kindly that you understand, and that $he'll be trained to address <<case "be the Schoolteacher">>($his office in $schoolroomName, where $he'll decide today's lesson), <<case "be the Stewardess">>($his office in $servantsQuartersName, where $he'll divvy out today's tasks), <<case "be the Milkmaid">>($dairyName, to check on the cattle), - <<case "be the Farmer">><<($farmyardName, to tend to the crops), + <<case "be the Farmer">>($farmyardName, to tend to the crops), <<case "be the Wardeness">>($cellblockName, to oversee the inmates), <<case "be your Concubine">>(your bed), <<case "be the Nurse">>($clinicName, to check on the patients), diff --git a/src/uncategorized/costsReport.tw b/src/uncategorized/costsReport.tw index 71acb6f942c242ce81e7ef69fd797dc0d9050f71..18fac0b47ff03305456796fa86ea478344ea1ae3 100644 --- a/src/uncategorized/costsReport.tw +++ b/src/uncategorized/costsReport.tw @@ -817,7 +817,7 @@ $nursery > 0 || $masterSuiteUpgradePregnancy > 0 || $incubator > 0 || <<set _total = 0>> <<set _SL = $slaves.length>> <<for $i = 0; $i < _SL; $i++>> <<capture $i>> - <br style="clear:both" /><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> + <br style="clear:both"><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> [[$slaves[$i].slaveName|Slave Interact][$activeSlave = $slaves[$i]]] will $slaves[$i].assignment. <<SlaveExpenses $slaves[$i]>> <</capture>> diff --git a/src/uncategorized/costsReportSlaves.tw b/src/uncategorized/costsReportSlaves.tw index 68276158fa87dfcb7adb8d0ab89bb9287d745226..3399fecd92dfe8a6509c01412263bd4e8a05718d 100644 --- a/src/uncategorized/costsReportSlaves.tw +++ b/src/uncategorized/costsReportSlaves.tw @@ -6,7 +6,7 @@ <<set _total = 0>> <<set _SL = $slaves.length>> <<for $i = 0; $i < _SL; $i++>> <<capture $i>> - <br style="clear:both" /><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> + <br style="clear:both"><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> [[$slaves[$i].slaveName|Slave Interact][$activeSlave = $slaves[$i]]] will $slaves[$i].assignment. <<SlaveExpenses $slaves[$i]>> <</capture>> diff --git a/src/uncategorized/matchmaking.tw b/src/uncategorized/matchmaking.tw index b10c5694b3e965c9f6986216ff474d51b5121702..dd884f98f2f89e5d5ab15c90e087add69e27bc40 100644 --- a/src/uncategorized/matchmaking.tw +++ b/src/uncategorized/matchmaking.tw @@ -378,7 +378,7 @@ Despite $his devotion and trust, $he is still a slave, and probably knows that $ <</if>> */ -<<if $seeImages == 1>><br style="clear:both" /><</if>> +<<if $seeImages == 1>><br style="clear:both"><</if>> <br><br>__Put $him with another worshipful <<if $eventSlave.relationship == -2>>emotionally bonded slave<<else>>emotional slut<</if>>:__ <<set $Flag = 1>> diff --git a/src/uncategorized/options.tw b/src/uncategorized/options.tw index 1c7c4fb5bbe624165e3cbd59ce83926c2c93674d..dde2ff781bdf5821fd11a45ddc39a616e08159b7 100644 --- a/src/uncategorized/options.tw +++ b/src/uncategorized/options.tw @@ -196,7 +196,7 @@ Main menu slave tabs are <</if>> <</if>> -<br /> +<br> The slave Quick list in-page scroll-to is <<if $useSlaveListInPageJSNavigation != 1>> @@ -254,7 +254,7 @@ Master Suite report details such as slave changes are <</if>> /* Accordion 000-250-006 */ -<br /> +<br> Accordion effects on weekly reports are <<if ($useAccordion != 1)>> @@.red;DISABLED.@@ [[Enable|Options][$useAccordion = 1]] diff --git a/src/uncategorized/repBudget.tw b/src/uncategorized/repBudget.tw index f539894bc0a2fa5cbd32f5e4658d66364d92e83b..6f86fe8de1ebfc179d8e8b4123e6c7793b98adae 100644 --- a/src/uncategorized/repBudget.tw +++ b/src/uncategorized/repBudget.tw @@ -14,7 +14,7 @@ <br> //Reputation is a difficult thing to quantify, <<= properTitle()>>. Here you see an overview of topics that interest people in the arcology, and in turn, reflect on your own reputation. The more symbols you see in a category, the more impact that category is having on your reputation lately.// -<br style="clear:both" /><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> +<br style="clear:both"><<if $lineSeparations == 0>><br><<else>><hr style="margin:0"><</if>> <br> //Your weekly reputation changes are as follows:// diff --git a/src/uncategorized/seRetirement.tw b/src/uncategorized/seRetirement.tw index 1e7675c756ae94c3146d47e68ec00a4433543e28..1ecab00f1cff96ed80a9cac3b81db3777390212a 100644 --- a/src/uncategorized/seRetirement.tw +++ b/src/uncategorized/seRetirement.tw @@ -210,7 +210,7 @@ Your arcology has gained a well-off citizen. <</if>> <</replace>> <</link>> -<br /> +<br> <<if _clonedSlave.relationship >= 4>> <<link "Send $his _girl2 into retirement with $him">> <<replace "#artFrame">> diff --git a/src/utility/descriptionWidgetsStyle.tw b/src/utility/descriptionWidgetsStyle.tw index 332c39fd64a2c10ea4c1dfdb09e46041161a8585..2b0b582edda330f37207b01447bd35e48c6a45e8 100644 --- a/src/utility/descriptionWidgetsStyle.tw +++ b/src/utility/descriptionWidgetsStyle.tw @@ -2532,7 +2532,7 @@ $His <<case "a string bikini" "cutoffs and a t-shirt" "a schoolgirl outfit" "a slutty maid outfit" "striped panties">> is curled into long flowing locks secured by hair ties with plastic buttons, bearing the garish inscription <<InscripDesc>> - <<case "a scalemail bikini" + <<case "a scalemail bikini">> is curled into long flowing locks, and topped by a gold headband. <<case "battledress">> is curled into floor-length locks secured by paracord. @@ -2606,7 +2606,7 @@ $His <<case "a string bikini" "cutoffs and a t-shirt" "a schoolgirl outfit" "a slutty maid outfit" "striped panties">> is curled into long locks secured by hair ties with plastic buttons, bearing the garish inscription <<InscripDesc>> - <<case "a scalemail bikini" + <<case "a scalemail bikini">> is curled into long flowing locks, and topped by a gold headband. <<case "battledress">> is curled into long locks, secured by paracord. @@ -2680,7 +2680,7 @@ $His <<case "a string bikini" "cutoffs and a t-shirt" "a schoolgirl outfit" "a slutty maid outfit" "striped panties">> is curled into short locks secured by hair ties with plastic buttons, bearing the garish inscription <<InscripDesc>> - <<case "a scalemail bikini" + <<case "a scalemail bikini">> is curled into short flowing locks, and topped by a gold headband. <<case "battledress">> is curled into short locks secured by paracord.