Skip to content

Commit 5d37db5

Browse files
committed
add common functions for parsing and evaluating RUL2 code
1 parent ed98e39 commit 5d37db5

2 files changed

Lines changed: 148 additions & 30 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.sc4nam.module
2+
3+
import java.nio.file.{Files, Paths, Path}
4+
import io.github.memo33.metarules.meta.{RotFlip, EquivRule, Rule, IdTile}
5+
import RotFlip._
6+
import syntax.IdTile
7+
8+
object Rul2Model {
9+
10+
sealed trait Driveside
11+
case object Rhd extends Driveside
12+
case object Lhd extends Driveside
13+
case object RhdAndLhd extends Driveside
14+
15+
def isRulFile(path: Path) = {
16+
val s = path.getFileName().toString()
17+
s.endsWith(".txt") || s.endsWith(".rul")
18+
}
19+
20+
/** Iterate all RUL files in a directory tree, following the order of the NAMControllerCompiler,
21+
* see https://github.com/memo33/NAMControllerCompiler/blob/4c375beaacf2d69aee96d923d364e47128b09fd3/src/controller/tasks/CollectRULsTask.java#L60
22+
*/
23+
def iterateRulFiles(directory: Path): Iterator[Path] = {
24+
import scala.jdk.StreamConverters._
25+
val children: Seq[Path] = Files.list(directory).toScala(Seq).sorted
26+
children.iterator.flatMap { child =>
27+
if (Files.isDirectory(child))
28+
iterateRulFiles(child)
29+
else if (isRulFile(child))
30+
Iterator(child)
31+
else
32+
Iterator.empty
33+
}
34+
}
35+
36+
def drivesideOfFile(path: Path): Driveside = {
37+
val name = path.getFileName().toString()
38+
if (name.contains("rhd.")) Rhd
39+
else if (name.contains("lhd.")) Lhd
40+
else RhdAndLhd
41+
}
42+
43+
def parseRule(line: String): Option[Rule[IdTile]] = {
44+
val chunk = line.split(";|\\[", 2)(0).trim
45+
if (chunk.isEmpty) {
46+
None
47+
} else try {
48+
val ts = chunk.split(",|=").grouped(3).toSeq.map(tup =>
49+
IdTile(java.lang.Long.decode(tup(0)).toInt, RotFlip(tup(1).toInt, tup(2).toInt)))
50+
Some(Rule(ts(0), ts(1), ts(2), ts(3)))
51+
} catch {
52+
case _: IllegalArgumentException => // syntax errors in RUL2 code
53+
// throw new IllegalArgumentException(line)
54+
None
55+
}
56+
}
57+
58+
val lhdPrefix = ";###LHD###"
59+
val rhdPrefix = ";###RHD###"
60+
61+
def parseRuleWithRestrictedDriveside(line: String, driveside: Driveside): Option[(Rule[IdTile], Driveside)] = {
62+
val line1 = line.trim()
63+
if (line1.startsWith(lhdPrefix)) {
64+
if (driveside == Rhd) None else parseRule(line1.substring(lhdPrefix.length)).map(_ -> Lhd)
65+
} else if (line1.startsWith(rhdPrefix)) {
66+
if (driveside == Lhd) None else parseRule(line1.substring(rhdPrefix.length)).map(_ -> Rhd)
67+
} else {
68+
parseRule(line1).map(_ -> driveside)
69+
}
70+
}
71+
72+
def applyRule(rule: Rule[IdTile], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
73+
if (t0 == rule(0) && t1 == rule(1)) Some((rule(2), rule(3)))
74+
else if (t0 == rule(0) * R2F1 && t1 == rule(1) * R2F1) Some((rule(2) * R2F1, rule(3) * R2F1))
75+
else if (t0 == rule(1) * R0F1 && t1 == rule(0) * R0F1) Some((rule(3) * R0F1, rule(2) * R0F1))
76+
else if (t0 == rule(1) * R2F0 && t1 == rule(0) * R2F0) Some((rule(3) * R2F0, rule(2) * R2F0))
77+
else None
78+
}
79+
80+
def load(directory: Path): Rul2Model = {
81+
val rulesRhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
82+
val rulesLhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
83+
val rulesShared = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
84+
val lookupRuleRhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesRhd) // the two maps should be disjoint
85+
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesLhd) // the two maps should be disjoint
86+
87+
LOGGER.info(s"""Loading all RUL2 code for RHD and LHD from "$directory"""")
88+
iterateRulFiles(directory).foreach { path =>
89+
val drivesideFile = drivesideOfFile(path)
90+
scala.util.Using.resource(new java.util.Scanner(path.toFile(), "UTF-8")) { scanner =>
91+
while(scanner.hasNextLine()) {
92+
parseRuleWithRestrictedDriveside(scanner.nextLine(), drivesideFile) match {
93+
// only the first matching rule is loaded by the game, so we store only the first one read
94+
case Some((rule, Rhd)) =>
95+
val key = new EquivRule(rule)
96+
if (!rulesShared.contains(key) && !rulesRhd.contains(key))
97+
rulesRhd.addOne(key, rule)
98+
case Some((rule, Lhd)) =>
99+
val key = new EquivRule(rule)
100+
if (!rulesShared.contains(key) && !rulesLhd.contains(key))
101+
rulesLhd.addOne(key, rule)
102+
case Some((rule, RhdAndLhd)) =>
103+
val key = new EquivRule(rule)
104+
if (!rulesShared.contains(key)) {
105+
if (!rulesRhd.contains(key) && !rulesLhd.contains(key))
106+
rulesShared.addOne(key, rule)
107+
else
108+
require(
109+
rulesRhd.contains(key) == rulesLhd.contains(key),
110+
s"There's an unexpected rule conflict that differs between RHD and LHD: $rule"
111+
)
112+
}
113+
case None => // ignore
114+
}
115+
}
116+
}
117+
}
118+
119+
new Rul2Model(lookupRuleRhd, lookupRuleLhd)
120+
}
121+
122+
def evaluateRulesOnce(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
123+
val key = new EquivRule(Rule(t0, t1, t0, t1))
124+
lookupRule.unapply(key).flatMap(rule => applyRule(rule, t0, t1))
125+
}
126+
127+
}
128+
129+
/** Holds all RUL2 code in memory. Instantiate this with `Rul2Model.load`. */
130+
class Rul2Model private (
131+
val lookupRuleRhd: PartialFunction[EquivRule, Rule[IdTile]],
132+
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]],
133+
)

src/main/scala/module/SanityChecker.scala

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import java.nio.file.{Files, Paths, Path}
44
import resource._
55
import io.github.memo33.metarules.meta.{RotFlip, Rule, EquivRule, IdTile}
66
import syntax.IdTile
7+
import Rul2Model.{iterateRulFiles, parseRule}
78

89
/** Run with `sbt "runMain com.sc4nam.module.SanityChecker"`.
910
*/
1011
object SanityChecker {
1112

13+
val linePatternIncludingNewlines = "(?<=\n|\r(?!\n))"
14+
1215
def main(args: Array[String]): Unit = {
1316
checkRedundantRul2()
1417
}
@@ -29,8 +32,8 @@ object SanityChecker {
2932

3033
// first cache all the RUL2 code generated by metarules
3134
LOGGER.info("caching RUL2 code generated by metarules")
32-
Files.list(Paths.get("target")).forEach { path =>
33-
if (isRulFile(path) && isMetaruleFile(path)) {
35+
iterateRulFiles(Paths.get("target")).foreach { path =>
36+
if (isMetaruleFile(path)) {
3437
for (scanner <- managed(new java.util.Scanner(path.toFile(), "UTF-8"))) {
3538
while(scanner.hasNextLine()) {
3639
parseRule(scanner.nextLine()).foreach { rule =>
@@ -43,18 +46,20 @@ object SanityChecker {
4346

4447
// then check handwritten RUL2 code for redundancies
4548
LOGGER.info("searching for redundant handwritten RUL2 code")
46-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
47-
if (isRulFile(path) && !isMetaruleFile(path)) {
49+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
50+
if (!isMetaruleFile(path)) {
4851
val tmpPath = path.resolveSibling(path.getFileName().toString() + ".tmp")
4952
val endsWithNewline = fileEndsWithNewline(path) // attempt to preserve missing newlines at end of files to avoid noise
50-
for (scanner <- managed(new java.util.Scanner(path.toFile(), "UTF-8")); printer <- managed(new java.io.PrintWriter(tmpPath.toFile(), "UTF-8"))) {
51-
while (scanner.hasNextLine()) {
52-
val line = scanner.nextLine()
53-
val printFun: String => Unit = if (!endsWithNewline && !scanner.hasNextLine()) printer.print else printer.println // preserve missing newlines at end of file
53+
scala.util.Using.resources(
54+
new java.util.Scanner(path.toFile(), "UTF-8").useDelimiter(linePatternIncludingNewlines),
55+
new java.io.PrintWriter(tmpPath.toFile(), "UTF-8")
56+
) { (lineScanner, printer) =>
57+
while (lineScanner.hasNext()) {
58+
val line = lineScanner.next()
5459
if (parseRule(line).exists(rule => seen(new EquivRule(rule)))) {
55-
printFun(s";$line; metaruled") // comments out the line
60+
printer.println(s";${line.stripLineEnd}; metaruled") // comments out the line
5661
} else {
57-
printFun(line)
62+
printer.print(line) // preserving original linebreaks
5863
}
5964
}
6065
}
@@ -71,26 +76,6 @@ object SanityChecker {
7176
metaruleFiles.contains(path.getFileName().toString())
7277
}
7378

74-
def isRulFile(path: Path) = {
75-
val s = path.getFileName().toString()
76-
s.endsWith(".txt") || s.endsWith(".rul")
77-
}
78-
79-
def parseRule(line: String): Option[Rule[IdTile]] = {
80-
val chunk = line.split(";|\\[", 2)(0).trim
81-
if (chunk.isEmpty) {
82-
None
83-
} else try {
84-
val ts = chunk.split(",|=").grouped(3).toSeq.map(tup =>
85-
IdTile(java.lang.Long.decode(tup(0)).toInt, RotFlip(tup(1).toInt, tup(2).toInt)))
86-
Some(Rule(ts(0), ts(1), ts(2), ts(3)))
87-
} catch {
88-
case _: IllegalArgumentException => // syntax errors in RUL2 code
89-
// throw new IllegalArgumentException(line)
90-
None
91-
}
92-
}
93-
9479
def fileEndsWithNewline(path: Path): Boolean = {
9580
managed(new java.io.RandomAccessFile(path.toFile(), "r")) acquireAndGet { raf =>
9681
val size = Files.size(path)

0 commit comments

Comments
 (0)