Skip to content

Commit

Permalink
feat: Add random presets completion and validation
Browse files Browse the repository at this point in the history
- Add completion for cfgspawnabletypes.xml based on cfgrandompresets.xml.
- Add validation for cfgspawnabletypes.xml based on cfgrandompresets.xml.
  • Loading branch information
rvost committed Dec 31, 2023
1 parent 46a1550 commit 7b562b3
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.rvost.lemminx.dayz;

import io.github.rvost.lemminx.dayz.model.LimitsDefinitionsModel;
import io.github.rvost.lemminx.dayz.model.RandomPresetsModel;
import org.eclipse.lsp4j.WorkspaceFolder;

import java.io.IOException;
Expand All @@ -20,18 +21,22 @@ public class DayzMissionService {
private final Map<String, Set<String>> missionFolders;
private volatile Map<String, Set<String>> limitsDefinitions;
private volatile Map<String, Set<String>> userLimitsDefinitions;
private volatile Map<String, Set<String>> randomPresets;
private final DirWatch watch;
private final ConcurrentLinkedQueue<MissionFolderEvent> folderChangeEvents;
private final ConcurrentLinkedQueue<Path> fileModifiedEvents;

private DayzMissionService(Path missionRoot,
Map<String, Set<String>> missionFolders,
Map<String, Set<String>> limitsDefinitions,
Map<String, Set<String>> userLimitsDefinitions) throws Exception {
Map<String, Set<String>> userLimitsDefinitions,
Map<String, Set<String>> randomPresets
) throws Exception {
this.missionRoot = missionRoot;
this.missionFolders = missionFolders;
this.limitsDefinitions = limitsDefinitions;
this.userLimitsDefinitions = userLimitsDefinitions;
this.randomPresets = randomPresets;
this.folderChangeEvents = new ConcurrentLinkedQueue<>();
this.fileModifiedEvents = new ConcurrentLinkedQueue<>();
this.watch = DirWatch.watchDirectory(missionRoot, this.folderChangeEvents, this.fileModifiedEvents);
Expand All @@ -45,7 +50,8 @@ public static DayzMissionService create(List<WorkspaceFolder> workspaceFolders)
var missionFiles = getMissionFiles(rootPath);
var limitsDefinitions = LimitsDefinitionsModel.getLimitsDefinitions(rootPath);
var userLimitsDefinitions = LimitsDefinitionsModel.getUserLimitsDefinitions(rootPath);
var service = new DayzMissionService(rootPath, missionFiles, limitsDefinitions, userLimitsDefinitions);
var randomPresets = RandomPresetsModel.getRandomPresets(rootPath);
var service = new DayzMissionService(rootPath, missionFiles, limitsDefinitions, userLimitsDefinitions, randomPresets);
new Thread(service::watchModifiedFiles).start();
return service;
}
Expand Down Expand Up @@ -101,6 +107,12 @@ private void watchModifiedFiles() {
userLimitsDefinitions = val;
}
}
if (path.getFileName().toString().equals(RandomPresetsModel.CFGRANDOMPRESETS_FILE)) {
var val = RandomPresetsModel.getRandomPresets(missionRoot);
if (!val.isEmpty()) {
randomPresets = val;
}
}
}
}
}
Expand Down Expand Up @@ -135,6 +147,9 @@ static boolean isCustomFile(Path path) {
return xmlMatcher.matches(path) && !folderMatcher.matches(path);
}

public Map<String, Set<String>> getRandomPresets() {
return randomPresets;
}
}

enum MissionFolderEventType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.github.rvost.lemminx.dayz.model;

import org.eclipse.lemminx.dom.DOMDocument;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static java.util.Map.entry;

public class RandomPresetsModel {
public static final String CFGRANDOMPRESETS_FILE = "cfgrandompresets.xml";
public static final String CARGO_TAG = "cargo";
public static final String ATTACHMENTS_TAG = "attachments";
public static final String NAME_ATTRIBUTE = "name";

public static boolean isRandomPresets(DOMDocument document) {
if (document == null) {
return false;
}
var uri = document.getDocumentURI();
return uri != null && uri.toLowerCase().endsWith(CFGRANDOMPRESETS_FILE);
}

public static Map<String, Set<String>> getRandomPresets(Path missionPath) {
var path = missionPath.resolve(CFGRANDOMPRESETS_FILE);

try (var input = Files.newInputStream(path)) {
return getRandomPresets(input);
} catch (IOException e) {
return Map.of();
}
}

public static Map<String, Set<String>> getRandomPresets(InputStream input) throws IOException {
try {
var db = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder();
var doc = db.parse(input);
if (doc.getDocumentElement() != null) {
doc.getDocumentElement().normalize();
var cargo = getValues(doc.getElementsByTagName(CARGO_TAG));
var attachments = getValues(doc.getElementsByTagName(ATTACHMENTS_TAG));
return new HashMap<>(Map.ofEntries(
entry(CARGO_TAG, cargo),
entry(ATTACHMENTS_TAG, attachments)
));
} else {
return Map.of();
}
} catch (ParserConfigurationException | SAXException e) {
return Map.of();
}
}

private static Set<String> getValues(NodeList nodes) {
var result = new HashSet<String>();
for (int i = 0; i < nodes.getLength(); i++) {
var node = nodes.item(i);
var attributes = node.getAttributes();
if (attributes != null) {
var nameAttr = attributes.getNamedItem(NAME_ATTRIBUTE);
if (nameAttr != null) {
result.add(nameAttr.getNodeValue());
}
}
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.rvost.lemminx.dayz.model;

import org.eclipse.lemminx.dom.DOMDocument;

public class SpawnableTypesModel {
public static final String SPAWNABLETYPES_TAG = "spawnabletypes";
public static final String PRESET_ATTRIBUTE = "preset";

public static boolean isSpawnableTypes(DOMDocument document) {
var docElement = document.getDocumentElement();
return docElement != null && SPAWNABLETYPES_TAG.equals(docElement.getNodeName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.rvost.lemminx.dayz.DayzMissionService;
import io.github.rvost.lemminx.dayz.model.CfgEconomyCoreModel;
import io.github.rvost.lemminx.dayz.model.LimitsDefinitionsModel;
import io.github.rvost.lemminx.dayz.model.SpawnableTypesModel;
import io.github.rvost.lemminx.dayz.model.TypesModel;
import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.dom.DOMDocument;
Expand Down Expand Up @@ -30,6 +31,8 @@ public void onAttributeValue(String valuePrefix, ICompletionRequest request, ICo
computeTypesCompletion(request, response, doc);
} else if (LimitsDefinitionsModel.isUserLimitsDefinitions(doc)) {
computeUserLimitsDefinitionsCompletion(request, response, doc);
} else if (SpawnableTypesModel.isSpawnableTypes(doc)) {
computeSpawnableTypesCompletion(request, response, doc);
}

}
Expand Down Expand Up @@ -128,4 +131,27 @@ private void computeUserLimitsDefinitionsCompletion(ICompletionRequest request,
}
}
}

private void computeSpawnableTypesCompletion(ICompletionRequest request, ICompletionResponse response, DOMDocument document) throws BadLocationException {
var editRange = request.getReplaceRange();
var offset = document.offsetAt(editRange.getStart());
var node = document.findNodeAt(offset);
var attr = node.findAttrAt(offset);

if (SpawnableTypesModel.PRESET_ATTRIBUTE.equals(attr.getName())) {
var availablePresets = missionService.getRandomPresets();
if (availablePresets.containsKey(node.getNodeName())) {
var options = availablePresets.get(node.getNodeName());
for (var option : options) {
var item = new CompletionItem();
var insertText = request.getInsertAttrValue(option);
item.setLabel(insertText);
item.setFilterText(insertText);
item.setKind(CompletionItemKind.Enum);
item.setTextEdit(Either.forLeft(new TextEdit(editRange, insertText)));
response.addCompletionItem(item);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.rvost.lemminx.dayz.DayzMissionService;
import io.github.rvost.lemminx.dayz.model.CfgEconomyCoreModel;
import io.github.rvost.lemminx.dayz.model.LimitsDefinitionsModel;
import io.github.rvost.lemminx.dayz.model.SpawnableTypesModel;
import io.github.rvost.lemminx.dayz.model.TypesModel;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMNode;
Expand Down Expand Up @@ -35,6 +36,8 @@ public void doDiagnostics(DOMDocument domDocument, List<Diagnostic> list, XMLVal
validateTypes(domDocument, list);
} else if (LimitsDefinitionsModel.isUserLimitsDefinitions(domDocument)) {
validateUserLimitsDefinitions(domDocument, list);
} else if (SpawnableTypesModel.isSpawnableTypes(domDocument)) {
validateSpawnableTypes(domDocument, list);
}
}

Expand Down Expand Up @@ -143,4 +146,25 @@ private static void validateUserNodes(List<DOMNode> nodes, Set<String> available
}
}
}

private void validateSpawnableTypes(DOMDocument document, List<Diagnostic> diagnostics) {
var randomPresets = missionService.getRandomPresets();
for (var typeNode : document.getDocumentElement().getChildren()) {
if (typeNode.hasChildNodes()) {
for (var node : typeNode.getChildren()) {
var kind = node.getNodeName();
if (node.hasAttribute(SpawnableTypesModel.PRESET_ATTRIBUTE) && randomPresets.containsKey(kind)) {
var attr = node.getAttributeNode(SpawnableTypesModel.PRESET_ATTRIBUTE);
if (!randomPresets.get(kind).contains(attr.getValue())) {
var attrValue = attr.getNodeAttrValue();
var range = XMLPositionUtility.createRange(attrValue);
String message = kind + " preset \"" + attr.getValue() + "\"" + " does not exist.";
diagnostics.add(new Diagnostic(range, message, DiagnosticSeverity.Error, ERROR_SOURCE, "invalid_random_preset"));
}
}
}
}
}
}

}

0 comments on commit 7b562b3

Please sign in to comment.