Skip to content

Commit d366585

Browse files
committed
Improved separate updating of configured and placed features
1 parent f1ea73c commit d366585

File tree

4 files changed

+141
-111
lines changed

4 files changed

+141
-111
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ lazy val root = project
55
.settings(
66
name := "minecraft-worldgen-updater",
77
organization := "de.martenschaefer",
8-
version := "2.0.0-pre.7",
8+
version := "2.0.0",
99
homepage := Some(url("https://github.com/mschae23/minecraft-worldgen-updater")),
1010

1111
scalaVersion := scala3Version,

src/main/scala/FeatureUpdater.scala

Lines changed: 107 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ import java.nio.file.{ Files, LinkOption, Path, Paths }
55
import java.util.Scanner
66
import scala.jdk.CollectionConverters.IterableHasAsScala
77
import scala.util.Using
8-
import de.martenschaefer.data.serialization.{ AlternativeError, Codec, Element, ElementError, ElementNode, JsonCodecs, RecordParseError, ValidationError }
8+
import de.martenschaefer.data.Result
9+
import de.martenschaefer.data.serialization.{ AlternativeError, Codec, Element, ElementError, ElementNode, Encoder, JsonCodecs, RecordParseError, ValidationError }
910
import de.martenschaefer.data.serialization.JsonCodecs.given
1011
import de.martenschaefer.data.util.DataResult.*
11-
import de.martenschaefer.data.util.Lifecycle
12+
import de.martenschaefer.data.util.*
1213
import feature.ConfiguredFeature
1314
import util.*
1415

1516
object FeatureUpdater {
16-
def process[T: Codec](originPath: Path, targetPath: Path,
17+
def process[T: Codec](inputPath: Path, outputPath: Path,
1718
processor: T => ProcessResult[T],
1819
getPostProcessWarnings: T => List[ElementError],
19-
fileNameRegex: String)(using flags: Flags): Unit = {
20-
if (originPath.equals(targetPath) && !Flag.AssumeYes.get) {
21-
println(colored("Input and output path are the same. The origin files will be overwritten.", Console.YELLOW))
20+
fileNameRegex: String, featureType: Option[String])(using flags: Flags): Option[WarningType] = {
21+
if (inputPath.equals(outputPath) && !Flag.AssumeYes.get) {
22+
println(colored("Input and output path are the same. The input files will be overwritten.", Console.YELLOW))
2223
println("Continue (y / N)? ")
2324

2425
Using(new Scanner(System.in)) { scanner =>
@@ -30,26 +31,18 @@ object FeatureUpdater {
3031
}
3132

3233
println(colored("Aborting.", Console.YELLOW))
33-
return
34+
return None
3435
}
3536
}
3637
}
3738

38-
if (Files.isDirectory(originPath)) {
39-
val warningType = this.processDirectory(originPath, targetPath, originPath,
40-
processor, getPostProcessWarnings, fileNameRegex)
41-
42-
val notice = warningType match {
43-
case WarningType.Error =>
44-
colored("Done with errors.", WarningType.Error.color)
45-
case WarningType.Warning =>
46-
colored("Done with warnings.", WarningType.Warning.color)
47-
case _ => colored("Done.", WarningType.Okay.color)
48-
}
49-
50-
println(notice)
51-
} else if (Files.isRegularFile(originPath))
52-
this.processFeatureFile(originPath, targetPath, processor, getPostProcessWarnings)
39+
if (Files.isDirectory(inputPath)) {
40+
Some(this.processDirectory(inputPath, outputPath, inputPath,
41+
processor, getPostProcessWarnings, fileNameRegex, featureType))
42+
} else if (Files.isRegularFile(inputPath))
43+
Some(this.processFeatureFile(inputPath, outputPath, processor, getPostProcessWarnings).warningType)
44+
else
45+
None
5346
}
5447

5548
def processFeatureFile[T: Codec](originFile: Path, targetFile: Path,
@@ -68,27 +61,30 @@ object FeatureUpdater {
6861
result
6962
}
7063

71-
def processDirectory[T: Codec](originDirectory: Path, targetDirectory: Path,
64+
def processDirectory[T: Codec](inputDirectory: Path, outputDirectory: Path,
7265
startingDirectory: Path,
7366
processor: T => ProcessResult[T],
7467
getPostProcessWarnings: T => List[ElementError],
75-
fileNameRegex: String, recursive: Boolean = false)(using flags: Flags): WarningType = {
76-
if (!Files.exists(originDirectory) || !Files.isDirectory(originDirectory))
77-
throw new IllegalArgumentException(s"${originDirectory.getFileName} doesn't exist or is not a directory")
68+
fileNameRegex: String, featureType: Option[String],
69+
recursive: Boolean = false)(using flags: Flags): WarningType = {
70+
if (!Files.exists(inputDirectory) || !Files.isDirectory(inputDirectory))
71+
throw new IllegalArgumentException(s"${inputDirectory.getFileName} doesn't exist or is not a directory")
7872

79-
if (Files.exists(targetDirectory) && !Files.isDirectory(targetDirectory))
80-
throw new IllegalArgumentException(s"${targetDirectory.getFileName} is not a directory")
73+
if (Files.exists(outputDirectory) && !Files.isDirectory(outputDirectory))
74+
throw new IllegalArgumentException(s"${outputDirectory.getFileName} is not a directory")
8175

82-
Files.createDirectories(targetDirectory)
76+
Files.createDirectories(outputDirectory)
8377

8478
var foundWarnings = false
8579
var foundErrors = false
8680

87-
Using(Files.newDirectoryStream(originDirectory)) { directoryStream =>
81+
val featureTypeString = featureType.map(_ + " ").getOrElse("")
82+
83+
Using(Files.newDirectoryStream(inputDirectory)) { directoryStream =>
8884
for (path <- directoryStream.asScala) {
8985
if (Flag.Recursive.get && Files.isDirectory(path)) {
90-
processDirectory(path, targetDirectory.resolve(path.getFileName), startingDirectory,
91-
processor, getPostProcessWarnings, fileNameRegex, recursive = true) match {
86+
processDirectory(path, outputDirectory.resolve(path.getFileName), startingDirectory,
87+
processor, getPostProcessWarnings, fileNameRegex, featureType, recursive = true) match {
9288
case WarningType.Error => foundErrors = true
9389
case WarningType.Warning => foundWarnings = true
9490
case _ =>
@@ -98,9 +94,9 @@ object FeatureUpdater {
9894
if (path.getFileName.toString.matches(fileNameRegex)) {
9995
val relativePath = startingDirectory.relativize(path)
10096

101-
println(s"[info] Processing $relativePath")
97+
println(s"[info] Processing $featureTypeString$relativePath")
10298

103-
val result = processFile[T](path, targetDirectory.resolve(path.getFileName), processor, getPostProcessWarnings)
99+
val result = processFile[T](path, outputDirectory.resolve(path.getFileName), processor, getPostProcessWarnings)
104100

105101
result match {
106102
case FileProcessResult.Errors(errors) => println()
@@ -122,39 +118,87 @@ object FeatureUpdater {
122118
else WarningType.Okay
123119
}
124120

125-
def processFile[T: Codec](originFile: Path, targetFile: Path,
126-
processor: T => ProcessResult[T],
127-
getPostProcessWarnings: T => List[ElementError])(using Flags): FileProcessResult = {
128-
var lifecycle = Lifecycle.Stable
129-
var foundWarnings = false
130-
var warnings: List[ElementError] = List.empty
131-
132-
val originString: String = try {
133-
read(originFile)
121+
def readFeature[T: Codec](inputPath: Path): Result[T] = {
122+
val inputString: String = try {
123+
read(inputPath)
134124
} catch {
135125
case e: IOException => {
136-
warnings ::= createErrorForException("read", Some("IO"), e)
137-
return FileProcessResult.Errors(warnings)
126+
return Failure(List(createErrorForException("read", Some("IO"), e)))
138127
}
139128

140129
case e: Exception => {
141-
warnings ::= createErrorForException("read", None, e)
142-
return FileProcessResult.Errors(warnings)
130+
return Failure(List(createErrorForException("read", None, e)))
131+
}
132+
}
133+
134+
try {
135+
Codec[T].decode(inputString)
136+
} catch {
137+
case e: Exception => {
138+
Failure(List(createErrorForException("decoding", None, e)))
143139
}
144140
}
141+
}
145142

146-
val originFeature: T = try {
147-
Codec[T].decode(originString) match {
148-
case Success(feature, l) => lifecycle += l
149-
feature
150-
case Failure(errors, _) => return FileProcessResult.Errors(errors.toList)
143+
def encodeFeature[T, E](feature: T)(using Codec[T], Encoder[Element, E]): Result[E] = {
144+
try {
145+
Codec[T].encode(feature)
146+
} catch {
147+
case e: Exception => {
148+
Failure(List(createErrorForException("encoding", None, e)))
151149
}
150+
}
151+
}
152+
153+
def writeFeature(targetPath: Path, feature: String): Option[List[ElementError]] = {
154+
try {
155+
write(targetPath, feature)
156+
None
152157
} catch {
158+
case e: IOException => {
159+
Some(List(createErrorForException("write", None, e)))
160+
}
161+
153162
case e: Exception => {
154-
warnings ::= createErrorForException("decoding", None, e)
155-
return FileProcessResult.Errors(warnings)
163+
Some(List(createErrorForException("write", None, e)))
156164
}
157165
}
166+
}
167+
168+
def getWarningForLifecycle(lifecycle: Lifecycle): Option[ElementError] = lifecycle match {
169+
case Lifecycle.Internal =>
170+
Some(ValidationError(_ => "Implementation details of Worldgen Updater are used."))
171+
case Lifecycle.Experimental =>
172+
Some(ValidationError(_ => "Experimental features are used."))
173+
case Lifecycle.Deprecated(since) =>
174+
Some(ValidationError(_ => s"Features that are deprecated since v$since are used."))
175+
case _ => None
176+
}
177+
178+
def printDone(warningType: WarningType)(using Flags): Unit = {
179+
println(warningType match {
180+
case WarningType.Error =>
181+
colored("Done with errors.", WarningType.Error.color)
182+
case WarningType.Warning =>
183+
colored("Done with warnings.", WarningType.Warning.color)
184+
case _ => colored("Done.", WarningType.Okay.color)
185+
})
186+
}
187+
188+
def processFile[T: Codec](inputPath: Path, outputPath: Path,
189+
processor: T => ProcessResult[T],
190+
getPostProcessWarnings: T => List[ElementError])(using Flags): FileProcessResult = {
191+
var lifecycle = Lifecycle.Stable
192+
var foundWarnings = false
193+
194+
val originFeature: T = readFeature(inputPath) match {
195+
case Success(feature, l) => lifecycle = l
196+
feature
197+
198+
case Failure(errors, _) => return FileProcessResult.Errors(errors)
199+
}
200+
201+
var warnings: List[ElementError] = List.empty
158202

159203
val targetFeature: T = try {
160204
val targetFeatureWriter: ProcessResult[T] = processor(originFeature)
@@ -170,51 +214,19 @@ object FeatureUpdater {
170214
}
171215
}
172216

173-
if (warnings.nonEmpty) foundWarnings = true
174-
175-
val targetFeatureString: String = try {
176-
Codec[T].encode(targetFeature)(using JsonCodecs.prettyJsonEncoder) match {
177-
case Success(json, l) => lifecycle += l
178-
json
179-
case Failure(errors, _) => {
180-
return FileProcessResult.Errors(errors.toList)
181-
}
182-
}
183-
} catch {
184-
case e: Exception => {
185-
warnings ::= createErrorForException("encoding", None, e)
186-
return FileProcessResult.Errors(warnings)
187-
}
188-
}
189-
190-
lifecycle match {
191-
case Lifecycle.Internal =>
192-
warnings = ValidationError(_ => "Implementation details of Worldgen Updater are used.") :: warnings
193-
foundWarnings = true
194-
case Lifecycle.Experimental =>
195-
warnings = ValidationError(_ => "Experimental features are used.") :: warnings
196-
foundWarnings = true
197-
case Lifecycle.Deprecated(since) =>
198-
warnings = ValidationError(_ => s"Features that are deprecated since v$since are used.") :: warnings
199-
foundWarnings = true
200-
case _ =>
217+
val targetFeatureString: String = encodeFeature(targetFeature)(using Codec[T], JsonCodecs.prettyJsonEncoder) match {
218+
case Success(s, l) => lifecycle += l
219+
s
220+
case Failure(errors, _) => return FileProcessResult.Errors(errors)
201221
}
202222

203-
try {
204-
write(targetFile, targetFeatureString)
205-
} catch {
206-
case e: IOException => {
207-
warnings ::= createErrorForException("write", Some("IO"), e)
208-
return FileProcessResult.Errors(warnings)
209-
}
223+
// Add lifecycle warning
224+
getWarningForLifecycle(lifecycle).foreach(error => warnings = error :: warnings)
210225

211-
case e: Exception => {
212-
warnings ::= createErrorForException("write", None, e)
213-
return FileProcessResult.Errors(warnings)
214-
}
215-
}
226+
// Write feature to file
227+
writeFeature(outputPath, targetFeatureString).foreach(errors => warnings = warnings ::: errors)
216228

217-
if (foundWarnings) FileProcessResult.Warnings(warnings) else FileProcessResult.Normal
229+
if (warnings.isEmpty) FileProcessResult.Normal else FileProcessResult.Warnings(warnings)
218230
}
219231

220232
private def createErrorForException(phase: String, kind: Option[String], e: Exception): ElementError =

src/main/scala/FileProcessResult.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package de.martenschaefer.minecraft.worldgenupdater
22

33
import de.martenschaefer.data.serialization.ElementError
44

5-
enum FileProcessResult {
6-
case Normal
7-
case Warnings(val warnings: List[ElementError])
8-
case Errors(val errors: List[ElementError])
5+
enum FileProcessResult(val warningType: WarningType) {
6+
case Normal extends FileProcessResult(WarningType.Okay)
7+
case Warnings(val warnings: List[ElementError]) extends FileProcessResult(WarningType.Warning)
8+
case Errors(val errors: List[ElementError]) extends FileProcessResult(WarningType.Error)
99

1010
def +(other: FileProcessResult): FileProcessResult = other match {
1111
case Errors(errors) => this match {

src/main/scala/UpdaterMain.scala

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,19 @@ object UpdaterMain {
2828
Flag.UpdateOnly -> ("update-only", Some('u')),
2929
Flag.Colored -> ("colored", None),
3030
Flag.Recursive -> ("recursive", Some('r')),
31-
Flag.Verbose -> ("verbose", Some('v')))) { flags =>
31+
Flag.Verbose -> ("verbose", Some('v')))) { flagsMap =>
32+
given flags: Flags = flagsMap
33+
3234
defaultedArgumentFlag("matches", None, CommandArgument.string("file name regex"), ".+\\.json$") { fileNameRegex =>
3335
literal("features") {
34-
literalFlag("legacy", None) { // Legacy; from configured features to placed features
36+
// Legacy; update configured features to placed features
37+
literalFlag("legacy", None) {
3538
argument(CommandArgument.string("input")) { input =>
3639
argument(CommandArgument.string("output")) { output =>
3740
result {
38-
this.processPlacedFeatures(input, output, fileNameRegex)(using flags)
41+
val warningType = this.processPlacedFeatures(input, output, fileNameRegex, legacy = true)
42+
43+
warningType.foreach(FeatureUpdater.printDone)
3944
}
4045
}
4146
}
@@ -48,9 +53,20 @@ object UpdaterMain {
4853
argument(CommandArgument.string("placed feature output")) { placedOutput =>
4954
result {
5055
// TODO find a better solution for this
51-
this.processConfiguredFeatures(configuredInput, configuredOutput, fileNameRegex)(using flags)
56+
var warningType = this.processConfiguredFeatures(configuredInput, configuredOutput, fileNameRegex)
5257
println()
53-
this.processPlacedFeatures(placedInput, placedOutput, fileNameRegex)(using flags)
58+
this.processPlacedFeatures(placedInput, placedOutput, fileNameRegex, legacy = false).foreach {
59+
case WarningType.Error => warningType = Some(WarningType.Error)
60+
61+
case WarningType.Warning =>
62+
if (warningType.contains(WarningType.Okay) || warningType.isEmpty)
63+
warningType = Some(WarningType.Warning)
64+
65+
case WarningType.Okay => if (warningType.isEmpty)
66+
warningType = Some(WarningType.Okay)
67+
}
68+
69+
warningType.foreach(FeatureUpdater.printDone)
5470
}
5571
}
5672
}
@@ -76,9 +92,9 @@ object UpdaterMain {
7692
}
7793
}
7894

79-
def processConfiguredFeatures(origin: String, target: String, fileNameRegex: String)(using flags: Flags): Unit = {
80-
val originPath = Paths.get(origin)
81-
val targetPath = Paths.get(target)
95+
def processConfiguredFeatures(origin: String, target: String, fileNameRegex: String)(using flags: Flags): Option[WarningType] = {
96+
val inputPath = Paths.get(origin)
97+
val outputPath = Paths.get(target)
8298

8399
val context = FeatureUpdateContext(flags(Flag.UpdateOnly))
84100

@@ -94,10 +110,10 @@ object UpdaterMain {
94110
val getFeaturePostProcessWarnings: ConfiguredFeature[_, _] => List[ElementError] =
95111
feature => feature.feature.getPostProcessWarnings(feature.config, context)
96112

97-
FeatureUpdater.process(originPath, targetPath, featureProcessor, getFeaturePostProcessWarnings, fileNameRegex)
113+
FeatureUpdater.process(inputPath, outputPath, featureProcessor, getFeaturePostProcessWarnings, fileNameRegex, Some("configured feature"))
98114
}
99115

100-
def processPlacedFeatures(origin: String, target: String, fileNameRegex: String)(using flags: Flags): Unit = {
116+
def processPlacedFeatures(origin: String, target: String, fileNameRegex: String, legacy: Boolean)(using flags: Flags): Option[WarningType] = {
101117
val originPath = Paths.get(origin)
102118
val targetPath = Paths.get(target)
103119

@@ -108,7 +124,8 @@ object UpdaterMain {
108124

109125
val getFeaturePostProcessWarnings: PlacedFeatureReference => List[ElementError] = _.getPostProcessWarnings(using context)
110126

111-
FeatureUpdater.process(originPath, targetPath, featureProcessor, getFeaturePostProcessWarnings, fileNameRegex)
127+
FeatureUpdater.process(originPath, targetPath, featureProcessor, getFeaturePostProcessWarnings, fileNameRegex,
128+
if (legacy) None else Some("placed feature"))
112129
}
113130

114131
def printHelp(): Unit = {
@@ -122,6 +139,7 @@ object UpdaterMain {
122139
printFlag(None, "colored", "Use colored output")
123140
printFlag(Some('r'), "recursive", "Recursively process features in subfolders")
124141
printFlag(Some('v'), "verbose", "Adds some debug information for parse errors")
142+
printFlag(None, "legacy", "Update only configured features to placed features")
125143
}
126144

127145
private def printFlag(shortFlag: Option[Char], flag: String, description: String): Unit =

0 commit comments

Comments
 (0)