From 7b562b339351306640294712a7696ef6c1d1ac6d Mon Sep 17 00:00:00 2001 From: rvost Date: Mon, 20 Nov 2023 19:57:36 +0300 Subject: [PATCH] feat: Add random presets completion and validation - Add completion for cfgspawnabletypes.xml based on cfgrandompresets.xml. - Add validation for cfgspawnabletypes.xml based on cfgrandompresets.xml. --- .../lemminx/dayz/DayzMissionService.java | 19 ++++- .../dayz/model/RandomPresetsModel.java | 78 +++++++++++++++++++ .../dayz/model/SpawnableTypesModel.java | 13 ++++ .../DayzCECompletionParticipant.java | 26 +++++++ .../DayzCEDiagnosticParticipant.java | 24 ++++++ 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/RandomPresetsModel.java create mode 100644 lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/SpawnableTypesModel.java diff --git a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/DayzMissionService.java b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/DayzMissionService.java index 06fa9c7..e26caf0 100644 --- a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/DayzMissionService.java +++ b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/DayzMissionService.java @@ -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; @@ -20,6 +21,7 @@ public class DayzMissionService { private final Map> missionFolders; private volatile Map> limitsDefinitions; private volatile Map> userLimitsDefinitions; + private volatile Map> randomPresets; private final DirWatch watch; private final ConcurrentLinkedQueue folderChangeEvents; private final ConcurrentLinkedQueue fileModifiedEvents; @@ -27,11 +29,14 @@ public class DayzMissionService { private DayzMissionService(Path missionRoot, Map> missionFolders, Map> limitsDefinitions, - Map> userLimitsDefinitions) throws Exception { + Map> userLimitsDefinitions, + Map> 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); @@ -45,7 +50,8 @@ public static DayzMissionService create(List 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; } @@ -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; + } + } } } } @@ -135,6 +147,9 @@ static boolean isCustomFile(Path path) { return xmlMatcher.matches(path) && !folderMatcher.matches(path); } + public Map> getRandomPresets() { + return randomPresets; + } } enum MissionFolderEventType { diff --git a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/RandomPresetsModel.java b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/RandomPresetsModel.java new file mode 100644 index 0000000..934accc --- /dev/null +++ b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/RandomPresetsModel.java @@ -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> 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> 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 getValues(NodeList nodes) { + var result = new HashSet(); + 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; + } +} diff --git a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/SpawnableTypesModel.java b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/SpawnableTypesModel.java new file mode 100644 index 0000000..551ee50 --- /dev/null +++ b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/model/SpawnableTypesModel.java @@ -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()); + } +} diff --git a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCECompletionParticipant.java b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCECompletionParticipant.java index 014cc17..9a6dc64 100644 --- a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCECompletionParticipant.java +++ b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCECompletionParticipant.java @@ -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; @@ -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); } } @@ -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); + } + } + } + } } diff --git a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCEDiagnosticParticipant.java b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCEDiagnosticParticipant.java index dffe423..8851e6f 100644 --- a/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCEDiagnosticParticipant.java +++ b/lemminx-dayz-ce/src/main/java/io/github/rvost/lemminx/dayz/participants/DayzCEDiagnosticParticipant.java @@ -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; @@ -35,6 +36,8 @@ public void doDiagnostics(DOMDocument domDocument, List list, XMLVal validateTypes(domDocument, list); } else if (LimitsDefinitionsModel.isUserLimitsDefinitions(domDocument)) { validateUserLimitsDefinitions(domDocument, list); + } else if (SpawnableTypesModel.isSpawnableTypes(domDocument)) { + validateSpawnableTypes(domDocument, list); } } @@ -143,4 +146,25 @@ private static void validateUserNodes(List nodes, Set available } } } + + private void validateSpawnableTypes(DOMDocument document, List 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")); + } + } + } + } + } + } + }