Skip to content

Commit 2c49cdb

Browse files
committed
add script for detecting redundant RUL2 adjacency code
1 parent 23a61cd commit 2c49cdb

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.sc4nam.scripts
2+
3+
import java.nio.file.{Files, Paths, Path}
4+
import com.sc4nam.module._
5+
import io.github.memo33.metarules.meta.{RotFlip, Rule, EquivRule, IdTile}
6+
import RotFlip._
7+
import syntax.IdTile
8+
import SanityChecker.{fileEndsWithNewline, linePatternIncludingNewlines}
9+
import Rul2Model.{iterateRulFiles, parseRuleWithRestrictedDriveside, Rhd, Lhd, RhdAndLhd, drivesideOfFile, applyRule}
10+
11+
/** Run with `SBT_OPTS="-Xmx2G" sbt "runMain com.sc4nam.scripts.RedundantAdjacenciesChecker"`.
12+
* Note that this increases the heap size for more memory.
13+
* Takes about 4 minutes.
14+
*/
15+
object RedundantAdjacenciesChecker {
16+
17+
/** Scans the Controller/RUL2/ folder for adjancy RUL2 code that is
18+
* redundant with the DLL RUL2 engine.
19+
*
20+
* The respective lines are commented out and tagged as "; redundant-adjacency".
21+
*
22+
* Make sure all your changes to files are committed to git beforehand,
23+
* as this modifies files in place.
24+
*
25+
* Afterwards, you need to manually look through the changes and remove the
26+
* redundant code.
27+
*/
28+
def main(args: Array[String]): Unit = {
29+
val rul2 = Rul2Model.load(Paths.get("Controller/RUL2"))
30+
checkRedundantAdjacencies(rul2)
31+
}
32+
33+
def checkRedundantAdjacencies(rul2: Rul2Model): Unit = {
34+
LOGGER.info("Searching for redundant adjacencies in RUL2 code")
35+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
36+
val drivesideFile = drivesideOfFile(path)
37+
val tmpPath = path.resolveSibling(path.getFileName().toString() + ".tmp")
38+
val endsWithNewline = fileEndsWithNewline(path) // attempt to preserve missing newlines at end of files to avoid noise
39+
scala.util.Using.resources(
40+
new java.util.Scanner(path.toFile(), "UTF-8").useDelimiter(linePatternIncludingNewlines),
41+
new java.io.PrintWriter(tmpPath.toFile(), "UTF-8")
42+
) { (lineScanner, printer) =>
43+
while (lineScanner.hasNext()) {
44+
val line = lineScanner.next()
45+
46+
val redundant =
47+
parseRuleWithRestrictedDriveside(line, drivesideFile) match {
48+
case Some((rule, Rhd)) => isRedundantAdjacency(rule, rul2.lookupRuleRhd)
49+
case Some((rule, Lhd)) => isRedundantAdjacency(rule, rul2.lookupRuleLhd)
50+
case Some((rule, RhdAndLhd)) =>
51+
val b = isRedundantAdjacency(rule, rul2.lookupRuleRhd)
52+
require(
53+
b == isRedundantAdjacency(rule, rul2.lookupRuleLhd),
54+
s"Redundancies should be the same for RHD and LHD: $rule" // hopefully this will always be the case
55+
)
56+
b
57+
case None => false // comments are not redundant
58+
}
59+
60+
if (redundant) {
61+
printer.println(s";${line.stripLineEnd}; redundant-adjacency") // comments out the line
62+
} else {
63+
printer.print(line) // preserving original linebreaks
64+
}
65+
}
66+
}
67+
Files.move(tmpPath, path, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
68+
}
69+
}
70+
71+
val orthogonalSurrogateTiles = Seq(
72+
IdTile(0x00004B00, R1F0), // Road
73+
IdTile(0x57000000, R1F0), // Dirtroad
74+
IdTile(0x05004B00, R1F0), // Street
75+
IdTile(0x5D540000, R1F0), // Rail
76+
IdTile(0x08031500, R1F0), // Lightrail
77+
IdTile(0x09004B00, R1F0), // Onewayroad (TODO or 0x5f940300?)
78+
IdTile(0x04006100, R3F0), // Avenue
79+
IdTile(0x0D031500, R1F0), // Monorail
80+
).flatMap(t => Seq(t, t * R2F0))
81+
82+
val diagonalSurrogateTiles = Seq(
83+
(IdTile(0x00000A00, R1F0), IdTile(0x00000A00, R3F0)), // Road
84+
(IdTile(0x57000200, R1F0), IdTile(0x57000200, R3F0)), // Dirtroad
85+
(IdTile(0x5F500200, R1F0), IdTile(0x5F500200, R3F0)), // Street
86+
(IdTile(0x5D540100, R1F0), IdTile(0x5D540100, R3F0)), // Rail
87+
(IdTile(0x08001A00, R1F0), IdTile(0x08001A00, R3F0)), // Lightrail
88+
(IdTile(0x09000A00, R1F0), IdTile(0x09000A00, R3F0)), // Onewayroad (TODO or 0x5f94....?)
89+
(IdTile(0x04000200, R2F0), IdTile(0x04003800, R0F0)), // Avenue~SW | Avenue~SharedDiagLeft
90+
(IdTile(0x04000200, R2F0), IdTile(0x04003800, R2F0)), // Avenue~SW | Avenue~SharedDiagLeft (here we probably need this extra rotation to remove corresponding RUL2 code)
91+
(IdTile(0x04003800, R0F0), IdTile(0x04000200, R0F0)), // Avenue~SharedDiagLeft | Avenue~SW
92+
(IdTile(0x04003800, R2F0), IdTile(0x04000200, R0F0)), // Avenue~SharedDiagLeft | Avenue~SW (here we probably need this extra rotation to remove corresponding RUL2 code)
93+
(IdTile(0x0D001A00, R1F0), IdTile(0x0D001A00, R3F0)), // Monorail
94+
).flatMap { case (a,b) => Seq(true, false).map { southBound =>
95+
val rot = if (southBound) R0F0 else R1F0
96+
(a * rot, b * rot, southBound)
97+
}}
98+
99+
/** Checks if adjacency overrides a -> b and b -> c exist (in that order and
100+
* direction) (where b is surrogate tile) and matches expected result
101+
* aExpected, cExpected.
102+
*/
103+
def connectingOrthOverridesExist(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], a: IdTile, b: IdTile, c: IdTile, aExpected: IdTile, cExpected: IdTile): Boolean = {
104+
Rul2Model.evaluateRulesOnce(lookupRule, a, b) match {
105+
case Some((a1, b1)) if a1 == a && a1.id != b1.id =>
106+
Rul2Model.evaluateRulesOnce(lookupRule, b1, c) match {
107+
case Some((b2, c2)) if b2 == b1 && b2.id != c2.id && c2 != c =>
108+
// found two overrides connecting a to c, so check if result of their application is as expected
109+
a1 == aExpected && c2 == cExpected
110+
case _ => false
111+
}
112+
case _ => false
113+
}
114+
}
115+
116+
/** Checks if adjacency overrides a -> b and b*rot -> c*rot and c -> d exist
117+
* and matches expected result aExpected, dExpected.
118+
* Here, b and c are diagonal surrogate tiles rotated for southbound or
119+
* northbound direction.
120+
*/
121+
def connectingDiagOverridesExist(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], a: IdTile, b: IdTile, c: IdTile, d: IdTile, southBound: Boolean, aExpected: IdTile, dExpected: IdTile): Boolean = {
122+
Rul2Model.evaluateRulesOnce(lookupRule, a, b) match {
123+
case Some((a1, b1)) if a1 == a && a1.id != b1.id =>
124+
val rot = if (southBound) R3F0 else R1F0
125+
Rul2Model.evaluateRulesOnce(lookupRule, b1 * rot, c * rot) match {
126+
case Some((b2rot, c2rot)) =>
127+
val b2 = b2rot * (R0F0 / rot)
128+
val c2 = c2rot * (R0F0 / rot)
129+
if (b2 == b1 && c2 != c) {
130+
Rul2Model.evaluateRulesOnce(lookupRule, c2, d) match {
131+
case Some((c3, d3)) if c3 == c2 && d3.id != c3.id && d3.id != b2.id && d3 != d =>
132+
a1 == aExpected && d3 == dExpected
133+
case _ => false
134+
}
135+
} else {
136+
false
137+
}
138+
case _ => false
139+
}
140+
case _ => false
141+
}
142+
}
143+
144+
def isRedundantAdjacency(rule: Rule[IdTile], lookupRule: PartialFunction[EquivRule, Rule[IdTile]]): Boolean = {
145+
val a = rule(0)
146+
def checkOrth() = {
147+
val c = rule(1)
148+
orthogonalSurrogateTiles.exists { b =>
149+
if (b.id == a.id || b.id == c.id) { // this would depend on the same rule
150+
false
151+
} else {
152+
(connectingOrthOverridesExist(lookupRule, a, b, c, rule(2), rule(3)) // a -> b -> c
153+
|| connectingOrthOverridesExist(lookupRule, c * R2F0, b * R2F0, a * R2F0, rule(3) * R2F0, rule(2) * R2F0)) // c -> b -> a
154+
}
155+
}
156+
}
157+
def checkDiag() = {
158+
val d = rule(1)
159+
diagonalSurrogateTiles.exists { case (b, c, southBound) =>
160+
if (c.id == a.id || b.id == d.id) { // this would depend on the same rule
161+
false
162+
} else {
163+
(connectingDiagOverridesExist(lookupRule, a, b, c, d, southBound, rule(2), rule(3)) // a -> b -> c -> d
164+
|| connectingDiagOverridesExist(lookupRule, d * R2F0, c * R2F0, b * R2F0, a * R2F0, southBound, rule(3) * R2F0, rule(2) * R2F0)) // d -> c -> b -> a
165+
}
166+
}
167+
}
168+
checkOrth() || checkDiag()
169+
}
170+
171+
}

0 commit comments

Comments
 (0)