Skip to content

Commit 9e5de63

Browse files
committed
move parser too
1 parent 2c7ccd1 commit 9e5de63

3 files changed

Lines changed: 480 additions & 0 deletions

File tree

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ tasks.withType(Javadoc).configureEach {
2323
}
2424

2525
dependencies {
26+
// Lightweight YAML parser (zero transitive deps) for the full wurst.build loader. The lightweight
27+
// WurstBuildConfig line-parser stays dependency-free; only WurstProjectConfigReader uses this.
28+
implementation 'org.yaml:snakeyaml:2.3'
29+
2630
testImplementation 'org.testng:testng:7.11.0'
2731
}
2832

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
package org.wurstscript.projectconfig;
2+
3+
import org.yaml.snakeyaml.LoaderOptions;
4+
import org.yaml.snakeyaml.Yaml;
5+
import org.yaml.snakeyaml.constructor.SafeConstructor;
6+
7+
import java.io.IOException;
8+
import java.io.Reader;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.ArrayList;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.Locale;
15+
import java.util.Map;
16+
17+
/**
18+
* Reads a full {@code wurst.build} file into a {@link WurstProjectConfigData}.
19+
* <p>
20+
* This is the heavyweight counterpart to {@link WurstBuildConfig}: where {@code WurstBuildConfig} only line-parses
21+
* the two launch-affecting settings ({@code scriptMode}, {@code wc3Patch}) without any dependency, this reader parses
22+
* the entire document (project name, dependencies, and the nested {@code buildMapData}) using snakeyaml. It exists so
23+
* tooling (the compiler) can load the complete project config without depending on the setup tool.
24+
* <p>
25+
* Parsing semantics mirror the historical setup-tool loader: unknown keys are ignored, a single dependency scalar is
26+
* accepted in place of a list, absent values fall back to the record defaults, and a blank {@code projectName}
27+
* defaults to the project folder name.
28+
*/
29+
public final class WurstProjectConfigReader {
30+
31+
private WurstProjectConfigReader() {
32+
}
33+
34+
/**
35+
* Loads the given {@code wurst.build} file.
36+
*
37+
* @return the parsed config, or {@code null} if the file does not exist.
38+
* @throws IOException if the file exists but cannot be read or is malformed.
39+
*/
40+
public static WurstProjectConfigData load(Path buildFile) throws IOException {
41+
if (buildFile == null || !Files.exists(buildFile)) {
42+
return null;
43+
}
44+
Map<?, ?> root;
45+
try (Reader reader = Files.newBufferedReader(buildFile)) {
46+
Object parsed = newYaml().load(reader);
47+
root = asMap(parsed);
48+
} catch (RuntimeException e) {
49+
throw new IOException("Could not read " + buildFile + ": malformed wurst.build", e);
50+
}
51+
Path parent = buildFile.toAbsolutePath().getParent();
52+
String folderName = parent == null || parent.getFileName() == null ? null : parent.getFileName().toString();
53+
return fromMap(root, folderName);
54+
}
55+
56+
/** Loads {@code <projectRoot>/wurst.build}; returns {@code null} if it does not exist. */
57+
public static WurstProjectConfigData loadFromProjectRoot(Path projectRoot) throws IOException {
58+
if (projectRoot == null) {
59+
return null;
60+
}
61+
return load(projectRoot.resolve(WurstBuildConfig.FILE_NAME));
62+
}
63+
64+
static WurstProjectConfigData fromMap(Map<?, ?> root, String fallbackProjectName) {
65+
String projectName = str(get(root, "projectName"));
66+
if (projectName.isBlank() && fallbackProjectName != null && !fallbackProjectName.isBlank()) {
67+
projectName = fallbackProjectName;
68+
}
69+
return new WurstProjectConfigData(
70+
projectName,
71+
stringList(get(root, "dependencies")),
72+
buildMapData(asMap(get(root, "buildMapData"))),
73+
parseEnum(ScriptMode.class, str(get(root, "scriptMode"))),
74+
str(get(root, "wc3Patch"))
75+
);
76+
}
77+
78+
private static WurstProjectBuildMapData buildMapData(Map<?, ?> m) {
79+
if (m == null) {
80+
return null;
81+
}
82+
return new WurstProjectBuildMapData(
83+
str(get(m, "name")),
84+
str(get(m, "fileName")),
85+
str(get(m, "author")),
86+
scenarioData(asMap(get(m, "scenarioData"))),
87+
optionFlags(asMap(get(m, "optionsFlags"))),
88+
players(asList(get(m, "players"))),
89+
forces(asList(get(m, "forces")))
90+
);
91+
}
92+
93+
private static WurstProjectBuildScenarioData scenarioData(Map<?, ?> m) {
94+
if (m == null) {
95+
return null;
96+
}
97+
return new WurstProjectBuildScenarioData(
98+
str(get(m, "description")),
99+
str(get(m, "suggestedPlayers")),
100+
loadingScreen(asMap(get(m, "loadingScreen")))
101+
);
102+
}
103+
104+
private static WurstProjectBuildLoadingScreenData loadingScreen(Map<?, ?> m) {
105+
if (m == null) {
106+
return null;
107+
}
108+
return new WurstProjectBuildLoadingScreenData(
109+
str(get(m, "model")),
110+
str(get(m, "background")),
111+
str(get(m, "title")),
112+
str(get(m, "subTitle")),
113+
str(get(m, "text"))
114+
);
115+
}
116+
117+
private static WurstProjectBuildOptionFlagsData optionFlags(Map<?, ?> m) {
118+
if (m == null) {
119+
return null;
120+
}
121+
return new WurstProjectBuildOptionFlagsData(
122+
bool(get(m, "hideMinimapPreview"), false),
123+
bool(get(m, "forcesFixed"), false),
124+
bool(get(m, "maskedAreasPartiallyVisible"), false),
125+
bool(get(m, "showWavesOnCliffShores"), false),
126+
bool(get(m, "showWavesOnRollingShores"), false),
127+
bool(get(m, "useItemClassificationSystem"), false)
128+
);
129+
}
130+
131+
private static List<WurstProjectBuildPlayer> players(List<?> list) {
132+
if (list == null) {
133+
return null;
134+
}
135+
List<WurstProjectBuildPlayer> result = new ArrayList<>();
136+
for (Object o : list) {
137+
Map<?, ?> p = asMap(o);
138+
if (p == null) {
139+
continue;
140+
}
141+
result.add(new WurstProjectBuildPlayer(
142+
intVal(get(p, "id"), 0),
143+
strOrNull(get(p, "name")),
144+
parseEnum(Race.class, str(get(p, "race"))),
145+
parseEnum(Controller.class, str(get(p, "controller"))),
146+
boolOrNull(get(p, "fixedStartLoc"))
147+
));
148+
}
149+
return result;
150+
}
151+
152+
private static List<WurstProjectBuildForce> forces(List<?> list) {
153+
if (list == null) {
154+
return null;
155+
}
156+
List<WurstProjectBuildForce> result = new ArrayList<>();
157+
for (Object o : list) {
158+
Map<?, ?> f = asMap(o);
159+
if (f == null) {
160+
continue;
161+
}
162+
result.add(new WurstProjectBuildForce(
163+
str(get(f, "name")),
164+
forceFlags(asMap(get(f, "flags"))),
165+
intList(get(f, "playerIds"))
166+
));
167+
}
168+
return result;
169+
}
170+
171+
private static WurstProjectBuildForceFlags forceFlags(Map<?, ?> m) {
172+
if (m == null) {
173+
return null;
174+
}
175+
WurstProjectBuildForceFlags d = WurstProjectBuildForceFlags.defaults();
176+
return new WurstProjectBuildForceFlags(
177+
bool(get(m, "allied"), d.allied()),
178+
bool(get(m, "alliedVictory"), d.alliedVictory()),
179+
bool(get(m, "sharedVision"), d.sharedVision()),
180+
bool(get(m, "sharedControl"), d.sharedControl()),
181+
bool(get(m, "sharedControlAdvanced"), d.sharedControlAdvanced())
182+
);
183+
}
184+
185+
// ---- snakeyaml + coercion helpers -------------------------------------------------------------------------
186+
187+
private static Yaml newYaml() {
188+
// SafeConstructor: parse plain data only (no arbitrary object instantiation from the document).
189+
return new Yaml(new SafeConstructor(new LoaderOptions()));
190+
}
191+
192+
private static Object get(Map<?, ?> map, String key) {
193+
return map == null ? null : map.get(key);
194+
}
195+
196+
private static Map<?, ?> asMap(Object o) {
197+
return o instanceof Map<?, ?> m ? m : null;
198+
}
199+
200+
private static List<?> asList(Object o) {
201+
if (o instanceof List<?> l) {
202+
return l;
203+
}
204+
if (o == null) {
205+
return null;
206+
}
207+
// Accept a single value where a list is expected.
208+
return Collections.singletonList(o);
209+
}
210+
211+
/** Empty string when absent/blank. */
212+
private static String str(Object o) {
213+
return o == null ? "" : o.toString().trim();
214+
}
215+
216+
/** Null when absent/blank (for fields whose absence is meaningful, e.g. player name). */
217+
private static String strOrNull(Object o) {
218+
String s = str(o);
219+
return s.isBlank() ? null : s;
220+
}
221+
222+
private static List<String> stringList(Object o) {
223+
List<?> list = asList(o);
224+
if (list == null) {
225+
return List.of();
226+
}
227+
List<String> result = new ArrayList<>();
228+
for (Object item : list) {
229+
if (item != null) {
230+
result.add(item.toString().trim());
231+
}
232+
}
233+
return result;
234+
}
235+
236+
private static List<Integer> intList(Object o) {
237+
List<?> list = asList(o);
238+
if (list == null) {
239+
return List.of();
240+
}
241+
List<Integer> result = new ArrayList<>();
242+
for (Object item : list) {
243+
Integer value = toInt(item);
244+
if (value != null) {
245+
result.add(value);
246+
}
247+
}
248+
return result;
249+
}
250+
251+
private static boolean bool(Object o, boolean defaultValue) {
252+
Boolean b = boolOrNull(o);
253+
return b == null ? defaultValue : b;
254+
}
255+
256+
private static Boolean boolOrNull(Object o) {
257+
if (o instanceof Boolean b) {
258+
return b;
259+
}
260+
if (o == null) {
261+
return null;
262+
}
263+
String s = o.toString().trim().toLowerCase(Locale.ROOT);
264+
if (s.equals("true")) {
265+
return Boolean.TRUE;
266+
}
267+
if (s.equals("false")) {
268+
return Boolean.FALSE;
269+
}
270+
return null;
271+
}
272+
273+
private static int intVal(Object o, int defaultValue) {
274+
Integer value = toInt(o);
275+
return value == null ? defaultValue : value;
276+
}
277+
278+
private static Integer toInt(Object o) {
279+
if (o instanceof Number n) {
280+
return n.intValue();
281+
}
282+
if (o == null) {
283+
return null;
284+
}
285+
try {
286+
return Integer.valueOf(o.toString().trim());
287+
} catch (NumberFormatException e) {
288+
return null;
289+
}
290+
}
291+
292+
private static <E extends Enum<E>> E parseEnum(Class<E> type, String value) {
293+
if (value == null || value.isBlank()) {
294+
return null;
295+
}
296+
try {
297+
return Enum.valueOf(type, value.trim().toUpperCase(Locale.ROOT));
298+
} catch (IllegalArgumentException e) {
299+
return null;
300+
}
301+
}
302+
}

0 commit comments

Comments
 (0)