|
| 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 | +) |
0 commit comments