Skip to content
Open
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
Expand Up @@ -64,6 +64,7 @@ public class UpdaterConfig extends MyYaml {
public YamlSection mods_updater_version;
public YamlSection mods_updater_async;
public YamlSection mods_update_check_name_for_mod_loader;
public YamlSection mods_updater_dayz_workshop_app_id;


public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderException, IllegalListException, NotLoadedException, IllegalKeyException, YamlWriterException {
Expand Down Expand Up @@ -230,6 +231,9 @@ public UpdaterConfig() throws IOException, DuplicateKeyException, YamlReaderExce
mods_update_check_name_for_mod_loader = put(name, "mods-updater", "check-name-for-mod-loader").setDefValues("false").setComments(
"Only relevant for determining if a curseforge mod release is forge or fabric.",
"If enabled additionally checks the mod name to see if it contains fabric or forge.");
mods_updater_dayz_workshop_app_id = put(name, "mods-updater", "dayz-workshop-app-id").setDefValues("221100").setComments(
"Steam workshop app-id used for DayZ mods.",
"When the mods path contains DayZ mod folders with meta.cpp files, AutoPlug reads their publishedid and updates them through SteamCMD.");

save();
unlockFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

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

public DayZWorkshopMod(File directory, String name, String publishedId) {
this.directory = directory;
this.name = name;
this.publishedId = publishedId;
}

public File getDirectory() {
return directory;
}

public String getName() {
return name;
}

public String getPublishedId() {
return publishedId;
}

@NotNull
public static List<DayZWorkshopMod> findIn(File dir) throws IOException {
if (!dir.exists()) throw new FileNotFoundException("Directory does not exist: " + dir);
List<DayZWorkshopMod> mods = new ArrayList<>();
File[] files = dir.listFiles();
if (files == null) return mods;
Arrays.sort(files, Comparator.comparing(File::getName));
for (File file : files) {
if (!file.isDirectory()) continue;
File metaFile = new File(file, "meta.cpp");
if (metaFile.exists())
mods.add(readFromMeta(file, metaFile));
}
return mods;
}

static DayZWorkshopMod readFromMeta(File modDir, File metaFile) throws IOException {
String name = modDir.getName();
String publishedId = null;
for (String line : Files.readAllLines(metaFile.toPath(), StandardCharsets.UTF_8)) {
String trimmedLine = line.trim();
if (trimmedLine.isEmpty() || trimmedLine.startsWith("//")) continue;

int equalsIndex = trimmedLine.indexOf('=');
if (equalsIndex < 0) continue;

String key = trimmedLine.substring(0, equalsIndex).trim();
String value = trimmedLine.substring(equalsIndex + 1).trim();
int semicolonIndex = value.indexOf(';');
if (semicolonIndex < 0) continue;
value = value.substring(0, semicolonIndex).trim();
if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2)
value = value.substring(1, value.length() - 1);

if (key.equals("name")) name = value;
if (key.equals("publishedid")) publishedId = value;
}

if (publishedId == null || !publishedId.matches("\\d+"))
throw new IOException("Failed to read publishedid from " + metaFile);

return new DayZWorkshopMod(modDir, name, publishedId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
import com.osiris.autoplug.client.tasks.updater.plugins.ResourceFinder;
import com.osiris.autoplug.client.tasks.updater.search.SearchResult;
import com.osiris.autoplug.client.utils.GD;
import com.osiris.autoplug.client.utils.SteamCMD;
import com.osiris.autoplug.client.utils.UtilsFile;
import com.osiris.autoplug.client.utils.UtilsMinecraft;
import com.osiris.betterthread.BThread;
import com.osiris.betterthread.BThreadManager;
import com.osiris.betterthread.BWarning;
import com.osiris.dyml.YamlSection;
import com.osiris.dyml.exceptions.DuplicateKeyException;
import com.osiris.jlib.logger.AL;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.NotNull;

import java.io.DataInputStream;
Expand Down Expand Up @@ -81,7 +84,14 @@ public void runAtStart() throws Exception {
setStatus("Fetching latest mod data...");

userProfile = updaterConfig.mods_updater_profile.asString();
this.allMods.addAll(new UtilsMinecraft().getMods(FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString())));
File modsDir = FileManager.convertRelativeToAbsolutePath(updaterConfig.mods_updater_path.asString());
List<DayZWorkshopMod> dayZWorkshopMods = DayZWorkshopMod.findIn(modsDir);
if (!dayZWorkshopMods.isEmpty()) {
doDayZWorkshopUpdateLogic(dayZWorkshopMods);
return;
}

this.allMods.addAll(new UtilsMinecraft().getMods(modsDir));

for (MinecraftMod installedMod :
allMods) {
Expand Down Expand Up @@ -337,6 +347,57 @@ else if (code == SearchResult.Type.RESOURCE_NOT_FOUND)

}

private void doDayZWorkshopUpdateLogic(@NotNull List<DayZWorkshopMod> dayZWorkshopMods) throws Exception {
setMax(dayZWorkshopMods.size());
String configuredWorkshopAppId = updaterConfig.mods_updater_dayz_workshop_app_id.asString();
String workshopAppId = configuredWorkshopAppId == null || configuredWorkshopAppId.isEmpty() ? "221100" : configuredWorkshopAppId;

if (userProfile.equals(notifyProfile)) {
for (DayZWorkshopMod mod : dayZWorkshopMods)
addInfo("NOTIFY: DayZ mod '" + mod.getName() + "' can be updated with Steam Workshop item " + mod.getPublishedId() + ".");
finish("Found " + dayZWorkshopMods.size() + " DayZ workshop mods.");
return;
}

SteamCMD steamCMD = new SteamCMD();
int successfulUpdates = 0;
int checkedMods = 0;
for (DayZWorkshopMod mod : dayZWorkshopMods) {
checkedMods++;
setStatus("Updating DayZ mod '" + mod.getName() + "' (" + checkedMods + "/" + dayZWorkshopMods.size() + ")...");
boolean isSuccess = steamCMD.installOrUpdateWorkshopItem(workshopAppId, mod.getPublishedId(), line -> {
AL.debug(this.getClass(), "SteamCMD-Out: " + line);
setStatus(line);
}, errLine -> {
AL.debug(this.getClass(), "SteamCMD-Err-Out: " + errLine);
setStatus(errLine);
addWarning(errLine);
});
if (!isSuccess) {
addWarning("Failed to update DayZ mod '" + mod.getName() + "' via SteamCMD.");
continue;
}

File downloadedDir = steamCMD.getWorkshopItemDir(workshopAppId, mod.getPublishedId());
if (userProfile.equals(manualProfile)) {
addInfo("MANUAL: Downloaded DayZ mod '" + mod.getName() + "' to " + downloadedDir.getAbsolutePath() + ".");
} else {
setStatus("Copying DayZ mod '" + mod.getName() + "' into " + mod.getDirectory().getAbsolutePath() + "...");
FileUtils.copyDirectory(downloadedDir, mod.getDirectory());
addInfo("Updated DayZ mod '" + mod.getName() + "' from Steam Workshop item " + mod.getPublishedId() + ".");
}
successfulUpdates++;
}

if (successfulUpdates == dayZWorkshopMods.size()) {
setSuccess(true);
finish("Updated " + successfulUpdates + " DayZ workshop mods.");
} else {
setSuccess(false);
finish("Updated " + successfulUpdates + "/" + dayZWorkshopMods.size() + " DayZ workshop mods.");
}
}

private void doDownloadLogic(@NotNull MinecraftMod mod, SearchResult result) {
SearchResult.Type code = result.type;
String type = result.getDownloadType(); // The file type to download (Note: When 'external' is returned nothing will be downloaded. Working on a fix for this!)
Expand Down
61 changes: 58 additions & 3 deletions src/main/java/com/osiris/autoplug/client/utils/SteamCMD.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class SteamCMD {

private static final String STEAMCMD_WORKSHOP_COMMAND = "+login {LOGIN} +workshop_download_item {WORKSHOP_APP} {WORKSHOP_ITEM} validate +quit";
private final String steamcmdArchive = "steamcmd" + (isWindows ? ".zip" : isMac ? "_osx.tar.gz" : "_linux.tar.gz");
private final String steamcmdExtension = isWindows ? ".exe" : ".sh";
private final String steamcmdExecutable = "steamcmd" + steamcmdExtension;
Expand Down Expand Up @@ -115,8 +116,7 @@ public boolean installOrUpdateServer(String appId, Consumer<String> onLog, Consu
AL.debug(this.getClass(), "Installing app " + appId + "...");
onLog.accept("Installing app " + appId + "...");

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

File gameInstallDir = new File(dirSteamServersDownloads + "/" + appId);
gameInstallDir.mkdirs();
Expand Down Expand Up @@ -159,10 +159,65 @@ public boolean installOrUpdateServer(String appId, Consumer<String> onLog, Consu
}
}

public boolean installOrUpdateWorkshopItem(String workshopAppId, String workshopItemId, Consumer<String> onLog, Consumer<String> onLogErr) {
try {
if (!installIfNeeded()) return false;
AL.debug(this.getClass(), "Installing workshop item " + workshopItemId + " for app " + workshopAppId + "...");
onLog.accept("Installing workshop item " + workshopItemId + "...");

String command = buildWorkshopItemCommand(getLogin(), workshopAppId, workshopItemId);
List<String> logLines = new ArrayList<>();
List<String> logErrLines = new ArrayList<>();
AtomicBoolean isFinished = new AtomicBoolean(false);
AtomicBoolean isSuccess = new AtomicBoolean(true);
AsyncTerminal terminal = new AsyncTerminal(destDir, line -> {
onLog.accept(line);
logLines.add(line);
String lowerLine = line.toLowerCase();
if (lowerLine.startsWith("success.") && lowerLine.contains("item " + workshopItemId.toLowerCase()))
isFinished.set(true);
if (lowerLine.startsWith("error!")) {
isSuccess.set(false);
isFinished.set(true);
}
}, line -> {
onLogErr.accept(line);
logErrLines.add(line);
}, destExe.getAbsolutePath() + " " + command);

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

public File getWorkshopItemDir(String workshopAppId, String workshopItemId) {
return new File(destDir + "/steamapps/workshop/content/" + workshopAppId + "/" + workshopItemId);
}

static String buildWorkshopItemCommand(String login, String workshopAppId, String workshopItemId) {
return STEAMCMD_WORKSHOP_COMMAND
.replace("{LOGIN}", login)
.replace("{WORKSHOP_APP}", workshopAppId)
.replace("{WORKSHOP_ITEM}", workshopItemId);
}

private String getLogin() throws NotLoadedException, YamlReaderException, YamlWriterException, IOException, IllegalKeyException, DuplicateKeyException, IllegalListException {
String login = new UpdaterConfig().server_steamcmd_login.asString();
if (login == null || login.isEmpty()) login = "anonymous";
return login;
}

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

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DayZWorkshopModTest {

@TempDir
Path tempDir;

@Test
void readsPublishedIdAndNameFromMetaCpp() throws Exception {
File modDir = tempDir.resolve("@CF").toFile();
modDir.mkdirs();
File metaFile = writeMeta(modDir,
"protocol = 1;",
"publishedid = 1559212036;",
"name = \"CF\";",
"timestamp = 5249804932187309401;");

DayZWorkshopMod mod = DayZWorkshopMod.readFromMeta(modDir, metaFile);

assertEquals(modDir, mod.getDirectory());
assertEquals("CF", mod.getName());
assertEquals("1559212036", mod.getPublishedId());
}

@Test
void findsModsWithMetaCppInDirectSubdirectories() throws Exception {
File firstModDir = tempDir.resolve("@CF").toFile();
File secondModDir = tempDir.resolve("@CommunityOnlineTools").toFile();
File ignoredDir = tempDir.resolve("keys").toFile();
firstModDir.mkdirs();
secondModDir.mkdirs();
ignoredDir.mkdirs();
writeMeta(firstModDir, "publishedid = 1559212036;", "name = \"CF\";");
writeMeta(secondModDir, "publishedid = 1564026768;", "name = \"Community-Online-Tools\";");

List<DayZWorkshopMod> mods = DayZWorkshopMod.findIn(tempDir.toFile());

assertEquals(2, mods.size());
assertEquals("1559212036", mods.get(0).getPublishedId());
assertEquals("1564026768", mods.get(1).getPublishedId());
}

private File writeMeta(File modDir, String... lines) throws Exception {
File metaFile = new File(modDir, "meta.cpp");
Files.write(metaFile.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8);
return metaFile;
}
}
11 changes: 10 additions & 1 deletion src/test/java/com/osiris/autoplug/client/utils/SteamCMDTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SteamCMDTest {

@Test
void buildsWorkshopItemCommand() {
String command = SteamCMD.buildWorkshopItemCommand("anonymous", "221100", "1559212036");

assertEquals("+login anonymous +workshop_download_item 221100 1559212036 validate +quit", command);
}

@Test
void installSteamcmd() throws IOException {
UtilsTest.init();
new SteamCMD().installIfNeeded();
}
}
}