Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2024 Osiris-Team.
* All rights reserved.
*
* This software is copyrighted work, licensed under the terms
* of the MIT-License. Consult the "LICENSE" file for details.
*/

package com.osiris.autoplug.client.tasks.updater.mods;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DayZWorkshopModUpdater {
public static final String DAYZ_SERVER_APP_ID = "223350";
public static final String DAYZ_WORKSHOP_APP_ID = "221100";
private static final Pattern PUBLISHED_ID_PATTERN = Pattern.compile("(?m)^\\s*publishedid\\s*=\\s*\"?([0-9]+)\"?\\s*;?\\s*$");
private static final Pattern NAME_PATTERN = Pattern.compile("(?m)^\\s*name\\s*=\\s*\"?([^\";]+)\"?\\s*;?\\s*$");

private final File serverDir;
private final File workshopDownloadDir;

public DayZWorkshopModUpdater(File serverDir, File workshopDownloadDir) {
this.serverDir = serverDir;
this.workshopDownloadDir = workshopDownloadDir;
}

public File getWorkshopDownloadDir() {
return workshopDownloadDir;
}

public List<DayZWorkshopMod> findInstalledMods() throws IOException {
List<DayZWorkshopMod> mods = new ArrayList<>();
File[] children = serverDir.listFiles(File::isDirectory);
if (children == null) return mods;

for (File dir : children) {
File metaCpp = findMetaCpp(dir);
if (metaCpp == null) continue;
DayZWorkshopMod mod = parseMetaCpp(metaCpp, dir);
mods.add(mod);
}
return mods;
}

public DayZWorkshopMod parseMetaCpp(File metaCpp, File modDir) throws IOException {
String content = new String(Files.readAllBytes(metaCpp.toPath()), StandardCharsets.UTF_8);
String publishedId = matchRequired(PUBLISHED_ID_PATTERN, content, "publishedid", metaCpp);
String name = matchOptional(NAME_PATTERN, content);
if (name == null || name.trim().isEmpty()) name = modDir.getName();
return new DayZWorkshopMod(name.trim(), publishedId, modDir);
}

public List<String> getWorkshopIds(List<DayZWorkshopMod> mods) {
List<String> ids = new ArrayList<>();
for (DayZWorkshopMod mod : mods) {
ids.add(mod.publishedId);
}
return ids;
}

public int installDownloadedMods(List<DayZWorkshopMod> mods) throws IOException {
int installed = 0;
for (DayZWorkshopMod mod : mods) {
File downloadedModDir = getDownloadedWorkshopContentDir(mod);
if (!downloadedModDir.isDirectory()) {
throw new IOException("Downloaded DayZ Workshop mod was not found at " + downloadedModDir.getAbsolutePath());
}
replaceDirectory(downloadedModDir, mod.directory);
copyKeysIfPresent(mod.directory);
installed++;
}
return installed;
}

public File getDownloadedWorkshopContentDir(DayZWorkshopMod mod) {
return new File(workshopDownloadDir + "/steamapps/workshop/content/" + DAYZ_WORKSHOP_APP_ID + "/" + mod.publishedId);
}

private File findMetaCpp(File dir) {
File[] files = dir.listFiles(file -> file.isFile() && file.getName().equalsIgnoreCase("meta.cpp"));
if (files == null || files.length == 0) return null;
return files[0];
}

private void replaceDirectory(File sourceDir, File targetDir) throws IOException {
File backupDir = new File(targetDir.getParentFile(), targetDir.getName() + ".autoplug-backup");
deleteDirectory(backupDir);

boolean hadTarget = targetDir.exists();
if (hadTarget) moveDirectory(targetDir, backupDir);

try {
copyDirectory(sourceDir, targetDir);
if (hadTarget) deleteDirectory(backupDir);
} catch (IOException e) {
deleteDirectory(targetDir);
if (backupDir.exists()) moveDirectory(backupDir, targetDir);
throw e;
}
}

private void copyKeysIfPresent(File modDir) throws IOException {
File modKeysDir = new File(modDir, "keys");
File[] keys = modKeysDir.listFiles(file -> file.isFile()
&& file.getName().toLowerCase(Locale.ROOT).endsWith(".bikey"));
if (keys == null || keys.length == 0) return;

File serverKeysDir = new File(serverDir, "keys");
serverKeysDir.mkdirs();
for (File key : keys) {
Files.copy(key.toPath(), new File(serverKeysDir, key.getName()).toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}

private void copyDirectory(File sourceDir, File targetDir) throws IOException {
Path sourcePath = sourceDir.toPath();
Path targetPath = targetDir.toPath();
Files.walkFileTree(sourcePath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Files.createDirectories(targetPath.resolve(sourcePath.relativize(dir)));
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, targetPath.resolve(sourcePath.relativize(file)),
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
return FileVisitResult.CONTINUE;
}
});
}

private void deleteDirectory(File dir) throws IOException {
if (!dir.exists()) return;
Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) throw exc;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}

private void moveDirectory(File sourceDir, File targetDir) throws IOException {
Files.move(sourceDir.toPath(), targetDir.toPath(), StandardCopyOption.REPLACE_EXISTING);
}

private String matchRequired(Pattern pattern, String content, String fieldName, File file) throws IOException {
String value = matchOptional(pattern, content);
if (value == null) {
throw new IOException("Missing " + fieldName + " in " + file.getAbsolutePath());
}
return value;
}

private String matchOptional(Pattern pattern, String content) {
Matcher matcher = pattern.matcher(content);
if (!matcher.find()) return null;
return matcher.group(1);
}

public static class DayZWorkshopMod {
public final String name;
public final String publishedId;
public final File directory;

public DayZWorkshopMod(String name, String publishedId, File directory) {
this.name = name;
this.publishedId = publishedId;
this.directory = directory;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.osiris.autoplug.client.configs.GeneralConfig;
import com.osiris.autoplug.client.configs.UpdaterConfig;
import com.osiris.autoplug.client.managers.FileManager;
import com.osiris.autoplug.client.tasks.updater.mods.DayZWorkshopModUpdater;
import com.osiris.autoplug.client.tasks.updater.mods.DayZWorkshopModUpdater.DayZWorkshopMod;
import com.osiris.autoplug.client.tasks.updater.TaskDownload;
import com.osiris.autoplug.client.tasks.updater.search.GithubSearch;
import com.osiris.autoplug.client.tasks.updater.search.JenkinsSearch;
Expand All @@ -32,6 +34,7 @@

import java.io.File;
import java.io.IOException;
import java.util.List;

import static com.osiris.jprocesses2.util.OS.isWindows;

Expand Down Expand Up @@ -59,23 +62,19 @@ public void runAtStart() throws Exception {
profile = updaterConfig.server_updater_profile.asString();
serverSoftware = updaterConfig.server_software.asString();

serverVersion = updaterConfig.server_updater_version.asString();
if (serverVersion == null) serverVersion = new GeneralConfig().server_version.asString();
if (serverVersion == null) serverVersion = new UtilsMinecraft().getInstalledVersion();
if (serverVersion == null) throw new NullPointerException(GD.errorMsgFailedToGetMCVersion());
boolean isSteamAppId = isSteamAppId(updaterConfig.server_software.asString());
boolean isAlternativeUpdater = updaterConfig.server_github_repo_name.asString() != null
|| updaterConfig.server_jenkins_project_url.asString() != null;
if (!isSteamAppId && !isAlternativeUpdater) {
serverVersion = updaterConfig.server_updater_version.asString();
if (serverVersion == null) serverVersion = new GeneralConfig().server_version.asString();
if (serverVersion == null) serverVersion = new UtilsMinecraft().getInstalledVersion();
if (serverVersion == null) throw new NullPointerException(GD.errorMsgFailedToGetMCVersion());
}

setStatus("Searching for updates...");

boolean isSteamAppId = false;
try {
Integer.parseInt(updaterConfig.server_software.asString());
// Steam ids are only numbers, thus
// if this fails we know it's not a steam id
isSteamAppId = true;
} catch (Exception e) {
}

if (updaterConfig.server_github_repo_name.asString() != null || updaterConfig.server_jenkins_project_url.asString() != null) {
if (isAlternativeUpdater) {
doAlternativeUpdatingLogic();
} else {
if (isSteamAppId)
Expand Down Expand Up @@ -272,7 +271,53 @@ private void doSteamUpdaterLogic() throws Exception {
}
}

setStatus("Installed updated if needed (SteamCMD).");
if (DayZWorkshopModUpdater.DAYZ_SERVER_APP_ID.equals(updaterConfig.server_software.asString())) {
doDayZWorkshopModUpdate(steamCMD);
} else {
setStatus("Installed updated if needed (SteamCMD).");
}
setSuccess(true);
}

private boolean isSteamAppId(String software) {
try {
Integer.parseInt(software);
return true;
} catch (Exception e) {
return false;
}
}

private void doDayZWorkshopModUpdate(SteamCMD steamCMD) throws Exception {
DayZWorkshopModUpdater dayZModUpdater = new DayZWorkshopModUpdater(GD.WORKING_DIR, steamCMD.dirDayZWorkshopDownloads);
List<DayZWorkshopMod> installedMods = dayZModUpdater.findInstalledMods();
if (installedMods.isEmpty()) {
setStatus("No DayZ Workshop mods with meta.cpp files were found.");
addInfo("No DayZ Workshop mods with meta.cpp files were found.");
return;
}

setStatus("Updating " + installedMods.size() + " DayZ Workshop mod(s)...");
boolean isSuccess = steamCMD.installOrUpdateWorkshopItems(
DayZWorkshopModUpdater.DAYZ_WORKSHOP_APP_ID,
dayZModUpdater.getWorkshopIds(installedMods),
dayZModUpdater.getWorkshopDownloadDir(),
line -> {
AL.debug(this.getClass(), "SteamCMD-Workshop-Out: " + line);
setStatus(line);
},
errLine -> {
AL.debug(this.getClass(), "SteamCMD-Workshop-Err-Out: " + errLine);
setStatus(errLine);
addWarning(errLine);
});
if (!isSuccess) throw new Exception("Failed to update DayZ Workshop mods with SteamCMD.");

if (profile.equals("AUTOMATIC")) {
int installed = dayZModUpdater.installDownloadedMods(installedMods);
setStatus("Installed " + installed + " DayZ Workshop mod update(s).");
} else {
setStatus("Downloaded DayZ Workshop mod update(s) to " + dayZModUpdater.getWorkshopDownloadDir().getAbsolutePath() + ".");
}
}
}
70 changes: 68 additions & 2 deletions src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class SteamCMD {
public File destDir = new File(GD.WORKING_DIR + "/autoplug/system/steamcmd");
public File destExe = new File(destDir + "/" + steamcmdExecutable);
public File dirSteamServersDownloads = new File(GD.DOWNLOADS_DIR + "/steam-servers");
public File dirDayZWorkshopDownloads = new File(GD.DOWNLOADS_DIR + "/dayz-workshop");
public File destArchive = new File(destDir + "/" + steamcmdArchive);

public boolean isInstalled() {
Expand Down Expand Up @@ -140,7 +141,7 @@ public boolean installOrUpdateServer(String appId, Consumer<String> onLog, Consu
onLogErr.accept(line);
logErrLines.add(line);
},
destExe.getAbsolutePath() + " " + steamcmdCommand
quoteForTerminal(destExe.getAbsolutePath()) + " " + steamcmdCommand
.replace("{LOGIN}", login)
.replace("{DESTINATION}", gameInstallDir.getAbsolutePath())
.replace("{APP}", appId));
Expand All @@ -159,10 +160,75 @@ public boolean installOrUpdateServer(String appId, Consumer<String> onLog, Consu
}
}

public boolean installOrUpdateWorkshopItems(String workshopAppId, List<String> itemIds, File installDir,
Consumer<String> onLog, Consumer<String> onLogErr) {
try {
if (itemIds == null || itemIds.isEmpty()) return true;
if (!installIfNeeded()) return false;
AL.debug(this.getClass(), "Installing workshop items for app " + workshopAppId + "...");
onLog.accept("Installing " + itemIds.size() + " Steam Workshop item(s)...");

String login = new UpdaterConfig().server_steamcmd_login.asString();
if (login == null || login.isEmpty()) login = "anonymous";
if (installDir == null) installDir = dirDayZWorkshopDownloads;

installDir.mkdirs();
StringBuilder steamcmdWorkshopCommand = new StringBuilder(quoteForTerminal(destExe.getAbsolutePath()))
.append(" +login ").append(login)
.append(" +force_install_dir \"").append(installDir.getAbsolutePath()).append("\"");
for (String itemId : itemIds) {
steamcmdWorkshopCommand.append(" +workshop_download_item ")
.append(workshopAppId)
.append(" ")
.append(itemId);
}
steamcmdWorkshopCommand.append(" +quit");

String doneToken = "AUTOPLUG_STEAMCMD_WORKSHOP_DONE_" + System.nanoTime();
AtomicBoolean isFinished = new AtomicBoolean(false);
AtomicBoolean isSuccess = new AtomicBoolean(true);
AsyncTerminal terminal = new AsyncTerminal(destDir, line -> {
if (line.contains(doneToken)) {
isFinished.set(true);
return;
}
onLog.accept(line);
String lowerLine = line.toLowerCase();
if (lowerLine.startsWith("error!") || lowerLine.contains("workshop item download failed")) {
isSuccess.set(false);
isFinished.set(true);
}
}, line -> {
onLogErr.accept(line);
String lowerLine = line.toLowerCase();
if (lowerLine.startsWith("error!") || lowerLine.contains("workshop item download failed")) {
isSuccess.set(false);
isFinished.set(true);
}
}, steamcmdWorkshopCommand.toString(), "echo " + doneToken);

Thread thread = new Thread(() -> {
terminal.process.destroy();
});
Runtime.getRuntime().addShutdownHook(thread);
while (!isFinished.get()) Thread.sleep(100);
terminal.process.destroy();
Runtime.getRuntime().removeShutdownHook(thread);
return isSuccess.get();
} catch (Exception e) {
AL.warn(e);
return false;
}
}

private String quoteForTerminal(String value) {
return "\"" + value + "\"";
}

public String getResolutionForError(String error) {
for (Map.Entry<String, String> entry : errorResolutions.entrySet())
if (error.contains(entry.getKey())) return entry.getValue();
return "Unknown. :(";
}

}
}
Loading