Skip to content

Commit

Permalink
feat: import and export ai files
Browse files Browse the repository at this point in the history
  • Loading branch information
Scoppio committed Dec 31, 2024
1 parent c945812 commit ae16e42
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 63 deletions.
3 changes: 1 addition & 2 deletions megamek/data/ai/tw/decisions/proto_decisions.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
!<TWDecision>
--- !<TWDecision>
action: AttackUnit
weight: 1.0
decisionScoreEvaluator: !<TWDecisionScoreEvaluator>
Expand Down
6 changes: 2 additions & 4 deletions megamek/data/ai/tw/evaluators/proto_dse.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
!<TWDecisionScoreEvaluator>
--- !<TWDecisionScoreEvaluator>
name: "AttackEnemyInRange"
description: "Evaluate if the enemy is optimal for attack."
notes: "File for testing the load of the evaluator system."
Expand All @@ -18,8 +17,7 @@ considerations:
k: 10.0
c: 0.0
parameters: {}
---
!<TWDecisionScoreEvaluator>
--- !<TWDecisionScoreEvaluator>
name: "Foobar"
description: "Spam Spam"
notes: "File for testing the load of the evaluator system."
Expand Down
28 changes: 19 additions & 9 deletions megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4825,7 +4825,7 @@ Bot.commands.aggression=Aggression
Bot.commands.bravery=Bravery
Bot.commands.avoid=Self-Preservation
Bot.commands.caution=Piloting Caution
aiEditor.tree.title=Princess Data


#### TacOps movement and damage descriptions
TacOps.leaping.leg_damage=leaping (leg damage)
Expand All @@ -4836,17 +4836,27 @@ CommonMenuBar.AIEditorMenu=AI Editor
CommonMenuBar.aiEditor.New=New Profile
CommonMenuBar.aiEditor.Open=Open Profile
CommonMenuBar.aiEditor.RecentProfile=Recent Profiles
CommonMenuBar.aiEditor.Save=Save Profile
CommonMenuBar.aiEditor.SaveAs=Save Profile As
CommonMenuBar.aiEditor.Save=Save All Changes
CommonMenuBar.aiEditor.SaveAs=Save As
CommonMenuBar.aiEditor.ReloadFromDisk=Reload Profile from Disk
CommonMenuBar.aiEditor.Undo=Undo
CommonMenuBar.aiEditor.Redo=Redo
CommonMenuBar.aiEditor.NewDecision=New Decision
CommonMenuBar.aiEditor.NewConsideration=New Consideration
CommonMenuBar.aiEditor.NewDecisionScoreEvaluator=New Decision Score Evaluator
CommonMenuBar.aiEditor.Export=Export Profile
CommonMenuBar.aiEditor.ExportConsiderations=Export Considerations
CommonMenuBar.aiEditor.ExportDSE=Export Decision Score Evaluators
CommonMenuBar.aiEditor.Import=Import Profile
CommonMenuBar.aiEditor.ImportConsiderations=Import Considerations
CommonMenuBar.aiEditor.ImportDSE=Import Decision Score Evaluators
CommonMenuBar.aiEditor.Export=Export...
CommonMenuBar.aiEditor.Import=Import...
aiEditor.Profiles=Profiles
aiEditor.Decisions=Decisions
aiEditor.DecisionScoreEvaluators=Decision Score Evaluators
aiEditor.Considerations=Considerations
aiEditor.tree.title=Princess Data
aiEditor.save.title=Save
aiEditor.export.title=Export AI configurations
aiEditor.save.filenameExtension=Megamek Utility AI (.uai)
aiEditor.export.error.title=Error exporting AI profile
aiEditor.export.error.message=An error occurred while saving the AI configuration.
aiEditor.import.title=Import AI configurations

aiEditor.import.error.title=Error importing AI configuration
aiEditor.import.error.message=An error occurred while importing the AI configuration.
27 changes: 14 additions & 13 deletions megamek/src/megamek/ai/utility/Consideration.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@
*/
package megamek.ai.utility;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.*;
import megamek.client.bot.duchess.ai.utility.tw.considerations.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
Expand All @@ -46,7 +45,7 @@ public abstract class Consideration<IN_GAME_OBJECT,TARGETABLE> implements Named
private Curve curve;
@JsonProperty("parameters")
protected Map<String, Object> parameters = Collections.emptyMap();

@JsonIgnore
protected transient Map<String, Class<?>> parameterTypes = Collections.emptyMap();

public Consideration() {
Expand Down Expand Up @@ -84,21 +83,23 @@ public Map<String, Class<?>> getParameterTypes() {
return Map.copyOf(parameterTypes);
}

public Class<?> getParameterType(String key) {
return parameterTypes.get(key);
}

public void setParameters(Map<String, Object> parameters) {
var params = new HashMap<String, Object>();
for (var entry : parameters.entrySet()) {
var clazz = parameterTypes.get(entry.getKey());
if (clazz == null) {
throw new IllegalArgumentException("Unknown parameter: " + entry.getKey());
}
if (clazz.isAssignableFrom(entry.getValue().getClass())) {
if (clazz.isEnum() && entry.getValue() instanceof String value) {
var enumValues = ((Class<? extends Enum>) clazz).getEnumConstants();
for (var anEnum : enumValues) {
if (anEnum.toString().equalsIgnoreCase(value)) {
parameters.put(entry.getKey(), anEnum);
break;
}
}
} else if (!clazz.isAssignableFrom(entry.getValue().getClass())) {
throw new IllegalArgumentException("Invalid parameter type for " + entry.getKey() + ": " + entry.getValue().getClass());
}
params.put(entry.getKey(), entry.getValue());
}
this.parameters = Map.copyOf(parameters);
}
Expand Down
3 changes: 3 additions & 0 deletions megamek/src/megamek/ai/utility/Decision.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package megamek.ai.utility;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
Expand All @@ -37,7 +38,9 @@ public class Decision<IN_GAME_OBJECT, TARGETABLE> implements NamedObject{
private Action action;
private double weight;
private DecisionScoreEvaluator<IN_GAME_OBJECT, TARGETABLE> decisionScoreEvaluator;
@JsonIgnore
private transient double score;
@JsonIgnore
private transient DecisionContext<IN_GAME_OBJECT, TARGETABLE> decisionContext;

public Decision() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
import megamek.common.Configuration;
import megamek.logging.MMLogger;

import java.io.File;
import java.io.IOException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class TWUtilityAIRepository {
private static final MMLogger logger = MMLogger.create(TWUtilityAIRepository.class);
Expand Down Expand Up @@ -91,20 +95,168 @@ public void persistDataToUserData() {
persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values());
}

public void exportAiData(File zipOutput) throws IOException {
var tempFolder = Files.createTempDirectory("my-ai-export");
var tempFile = tempFolder.toFile();

createDirectoryStructureIfMissing(tempFile);
persistToFile(new File(tempFile, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values());
persistToFile(new File(tempFile, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values());
persistToFile(new File(tempFile, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values());
persistToFile(new File(tempFile, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values());

zipDirectory(tempFolder, zipOutput.toPath());
deleteRecursively(tempFolder);
}

public void importAiData(File zipInput) {
try {
unzipDirectory(zipInput.toPath());
loadUserDataRepository();
persistDataToUserData();
deleteUserTempFiles();
} catch (IOException e) {
logger.error(e, "Could not load data from file: {}", zipInput);

Check warning

Code scanning / CodeQL

Unused format argument Warning

This format call refers to 0 argument(s) but supplies 1 argument(s).
}
}

private void deleteUserTempFiles() throws IOException {
var userFolder = Configuration.userDataAiTwDir();

Files.walk(userFolder.toPath())
.sorted(Comparator.reverseOrder())
.filter(p -> p.toFile().getName().startsWith("temp_"))
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
e.printStackTrace();
}
});
}

private void deleteRecursively(Path path) throws IOException {
// Walk the directory in reverse, so we delete children before parent
Files.walk(path)
.sorted(Comparator.reverseOrder())
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
e.printStackTrace();
}
});
}

/**
* Create a new file, ensuring that it is within the destination directory.
* This covers against the vulnerability for zip slip attacks
* @param destinationDir
* @param zipEntry
* @return the new file
* @throws IOException
*/
public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
File destFile = new File(destinationDir, zipEntry.getName());

String destDirPath = destinationDir.getCanonicalPath();
String destFilePath = destFile.getCanonicalPath();

if (!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}

return destFile;
}

private void unzipDirectory(Path zipFile) throws IOException {
var destDir = Configuration.userDataAiTwDir();

byte[] buffer = new byte[1024];
var zis = new ZipInputStream(new FileInputStream(zipFile.toFile()));
ZipEntry zipEntry = zis.getNextEntry();

while (zipEntry != null) {
File newFile = newFile(destDir, zipEntry);
if (zipEntry.isDirectory()) {
if (!newFile.isDirectory() && !newFile.mkdirs()) {
throw new IOException("Failed to create directory " + newFile);
}
} else {
// fix for Windows-created archives
File parent = newFile.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IOException("Failed to create directory " + parent);

Check warning

Code scanning / CodeQL

Unused format argument Warning

This format call refers to 0 argument(s) but supplies 1 argument(s).
}
if (newFile.exists()) {
// rewrite the filename with current timestamp at the end before the extension
var newName = newFile.getName();
newName = "temp_" + newName;
newFile = new File(parent, newName);
}

// write file content
FileOutputStream fos = new FileOutputStream(newFile);
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
fos.close();
}
zipEntry = zis.getNextEntry();
}

zis.closeEntry();

Check warning

Code scanning / CodeQL

Unused format argument Warning

This format call refers to 0 argument(s) but supplies 1 argument(s).
zis.close();
}

private void zipDirectory(Path sourceDir, Path zipFile) throws IOException {
// Try-with-resources to ensure ZipOutputStream is closed
try (ZipOutputStream zs = new ZipOutputStream(Files.newOutputStream(zipFile))) {
// Walk the directory tree
Files.walk(sourceDir)
// Only zip up files, not directories themselves
.filter(path -> !Files.isDirectory(path))
.forEach(path -> {
// Create a zip entry with a relative path
ZipEntry zipEntry = new ZipEntry(sourceDir.relativize(path).toString());
try {
zs.putNextEntry(zipEntry);
Files.copy(path, zs);
zs.closeEntry();
} catch (IOException e) {
e.printStackTrace();
}
});
}
}


public List<TWDecision> getDecisions() {
return List.copyOf(decisions.values());
var orderedList = new ArrayList<>(decisions.values());
orderedList.sort(Comparator.comparing(TWDecision::getName));

return List.copyOf(orderedList);
}

public List<TWConsideration> getConsiderations() {
return List.copyOf(considerations.values());
var orderedList = new ArrayList<>(considerations.values());
orderedList.sort(Comparator.comparing(TWConsideration::getName));

return List.copyOf(orderedList);
}

public List<TWDecisionScoreEvaluator> getDecisionScoreEvaluators() {
return List.copyOf(decisionScoreEvaluators.values());
var orderedList = new ArrayList<>(decisionScoreEvaluators.values());
orderedList.sort(Comparator.comparing(TWDecisionScoreEvaluator::getName));

return List.copyOf(orderedList);
}

public List<TWProfile> getProfiles() {
return List.copyOf(profiles.values());
var orderedList = new ArrayList<>(profiles.values());
orderedList.sort(Comparator.comparing(TWProfile::getName));
return List.copyOf(orderedList);
}

public boolean hasDecision(String name) {
Expand Down Expand Up @@ -177,7 +329,7 @@ private void loadUserDataRepository() {

private void loadData(File directory) {
loadConsiderations(new File(directory, CONSIDERATIONS))
.forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration));
.forEach(twConsideration -> considerations.put(twConsideration.getName(), twConsideration));
loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach(
twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator));
loadDecisions(new File(directory, DECISIONS)).forEach(
Expand Down
2 changes: 1 addition & 1 deletion megamek/src/megamek/client/ui/swing/CommonMenuBar.java
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public CommonMenuBar() {
menu.addSeparator();

initMenuItem(aiEditorSave, menu, AI_EDITOR_SAVE);
initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS);
// initMenuItem(aiEditorSaveAs, menu, AI_EDITOR_SAVE_AS);
initMenuItem(aiEditorReloadFromDisk, menu, AI_EDITOR_RELOAD_FROM_DISK);
menu.addSeparator();

Expand Down
Loading

0 comments on commit ae16e42

Please sign in to comment.