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
9 changes: 7 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ plugins {
id 'application'
id 'io.freefair.lombok' version '9.1.0'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.28'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}

group = 'game.s4mc'
version = '1.0'
version = '1.1'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}

application {
mainClass = 'game.s4mc.Launcher'
Expand Down
22 changes: 15 additions & 7 deletions src/main/java/game/s4mc/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.BiConsumer;
import java.util.prefs.Preferences;

Expand Down Expand Up @@ -146,6 +148,7 @@ private List<Node> createTopBarSettings() {
languageSelector.setMaxWidth(120);
languageSelector.getItems().addAll(
"English",
"한국어",
"Русский",
"Українська"
);
Expand Down Expand Up @@ -222,7 +225,7 @@ private void processNextMod(List<Mod> queue, int index, Button btnControl) {
.thenAccept(remoteVersion -> {
Platform.runLater(() -> {
if (remoteVersion != null) {
boolean match = remoteVersion.equals(mod.getVersion());
boolean match = VersionChecker.versionsMatch(mod.getVersion(), remoteVersion);
mod.setUpToDate(match);
if (!match) {
mod.setNewVersion(remoteVersion);
Expand All @@ -233,12 +236,17 @@ private void processNextMod(List<Mod> queue, int index, Button btnControl) {
});
})
.whenComplete((_, _) -> {
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {
}

processNextMod(queue, index + 1, btnControl);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "s4mc-rate-limit");
t.setDaemon(true);
return t;
});
scheduler.schedule(
() -> processNextMod(queue, index + 1, btnControl),
VersionChecker.getRateLimitMs(),
java.util.concurrent.TimeUnit.MILLISECONDS
);
scheduler.shutdown();
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/game/s4mc/cell/ModListCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private void setupActions() {
if (remoteVersion == null) {
versionBadge.setStyle("-fx-background-color: #f39c12; -fx-background-radius: 20;");
} else {
boolean match = remoteVersion.equals(mod.getVersion());
boolean match = VersionChecker.versionsMatch(mod.getVersion(), remoteVersion);
mod.setUpToDate(match);
if (!match) {
mod.setNewVersion(remoteVersion);
Expand Down
127 changes: 76 additions & 51 deletions src/main/java/game/s4mc/manager/DataManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DataManager {
private static final Logger LOG = Logger.getLogger(DataManager.class.getName());
private static DataManager instance;

private static final String MODS_FILE = "mods.json";
private static final String RULES_FILE = "rules.json";
private static final String COVERS_DIR = "covers";
private static final Path DATA_DIR = Paths.get(System.getProperty("user.home"), ".sims4modchecker");
private static final String MODS_FILE = DATA_DIR.resolve("mods.json").toString();
private static final String RULES_FILE = DATA_DIR.resolve("rules.json").toString();
private static final String COVERS_DIR = DATA_DIR.resolve("covers").toString();
private final ObjectMapper mapper = new ObjectMapper();

@Getter
Expand All @@ -43,21 +47,41 @@ public static DataManager getInstance() {

private DataManager() {
createDirs();
migrateLegacyFiles();
}

private void createDirs() {
try {
Files.createDirectories(DATA_DIR);
Files.createDirectories(Paths.get(COVERS_DIR));
} catch (IOException e) {
e.printStackTrace();
LOG.log(Level.SEVERE, "Failed to create data directories", e);
}
}

private void migrateLegacyFiles() {
migrateSingleFile("mods.json", MODS_FILE);
migrateSingleFile("rules.json", RULES_FILE);
}

private void migrateSingleFile(String legacyName, String newPath) {
File legacy = new File(legacyName);
File target = new File(newPath);
if (legacy.exists() && !target.exists()) {
try {
Files.move(legacy.toPath(), target.toPath());
LOG.info("Migrated " + legacyName + " to " + newPath);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to migrate " + legacyName, e);
}
}
}

public void save() {
try {
mapper.writerWithDefaultPrettyPrinter().writeValue(new File(MODS_FILE), modList);
} catch (IOException e) {
e.printStackTrace();
LOG.log(Level.SEVERE, "Failed to save mods.json", e);
}
}

Expand All @@ -68,50 +92,40 @@ public void load() {

public void reloadRules() {
rulesList.clear();

rulesList.addAll(
// Schema.org -> <span itemprop="version">...</span>
new Rule(
"019b4232-629f-7994-9bfa-ab02357b5005",
"Synthira",
"Itemprop->Version",
Strategy.CSS_TEXT,
"[itemprop=version]"
),

// Версия файла: ...
new Rule(
"019b4232-7e56-7f5d-9c74-e13c51a42a5d",
"Synthira",
"Версия файла",
Strategy.REGEX_HTML,
"Версия файла:\\s*<[^>]+>([^<]+)"
),

// Обновлено: ...
new Rule(
"019b4232-8e16-797e-9fb9-f05a9e2a4811",
"Synthira",
"Дата обновления", Strategy.REGEX_TEXT,
"Обновлено:\\s*(\\d{2}\\.\\d{2}\\.\\d{4}(?:,\\s*\\d{2}:\\d{2})?)"
),

// Обновлена версия X
new Rule(
"019b4232-b0ee-7cb6-9bc6-27f938ba8c55",
"Synthira", "Обновлена версия",
Strategy.REGEX_TEXT,
"Обновлена версия\\s+([0-9a-zA-Z\\./]+)"
),

// v1.0.0
new Rule(
"019b4232-c111-75b5-8b44-54943df3509f",
"Synthira",
"vX.X.X",
Strategy.REGEX_TEXT,
"v?(\\d+\\.\\d+(?:\\.\\d+)?)"
)
// --- Synthira (original) ---
new Rule("019b4232-629f-7994-9bfa-ab02357b5005", "Synthira", "Itemprop->Version", Strategy.CSS_TEXT, "[itemprop=version]"),
new Rule("019b4232-7e56-7f5d-9c74-e13c51a42a5d", "Synthira", "Версия файла", Strategy.REGEX_HTML, "Версия файла:\\s*<[^>]+>([^<]+)"),
new Rule("019b4232-8e16-797e-9fb9-f05a9e2a4811", "Synthira", "Дата обновления", Strategy.REGEX_TEXT, "Обновлено:\\s*(\\d{2}\\.\\d{2}\\.\\d{4}(?:,\\s*\\d{2}:\\d{2})?)"),
new Rule("019b4232-b0ee-7cb6-9bc6-27f938ba8c55", "Synthira", "Обновлена версия", Strategy.REGEX_TEXT, "Обновлена версия\\s+([0-9a-zA-Z\\./]+)"),

// --- MCCC (deaderpool-mccc.com) ---
new Rule("a1000001-mccc-0001-0001-000000000001", "MCCC", "MCCC Download Page Version", Strategy.REGEX_TEXT, "MC Command Center\\s+(\\d{4}\\.\\d+\\.\\d+)"),

// --- WickedWhims (wickedwhimsmod.com / itch.io) ---
new Rule("a1000002-wwms-0001-0001-000000000001", "WickedWhims", "WickedWhims Version", Strategy.REGEX_TEXT, "WickedWhims\\s+(v\\d+[a-z]*)"),
new Rule("a1000002-wwms-0001-0002-000000000001", "WickedWhims", "WickedWhims itch.io", Strategy.REGEX_TEXT, "WickedWhims\\s+(v\\d+[a-z]*)\\s*-\\s*PUBLIC"),

// --- Basemental (basementalcc.com) ---
new Rule("a1000003-bsmn-0001-0001-000000000001", "Basemental", "Basemental Version", Strategy.REGEX_TEXT, "(\\d+\\.\\d+\\.\\d+)\\s*\\(PUBLIC\\)"),

// --- NisaK Wicked Perversions (wicked.cc) ---
new Rule("a1000004-nisa-0001-0001-000000000001", "NisaK", "Wicked Perversions Version", Strategy.REGEX_TEXT, "Current Version:\\s*([\\w.]+)"),
new Rule("a1000004-nisa-0001-0002-000000000001", "NisaK", "WP Download File", Strategy.REGEX_TEXT, "NisaWickedPerversionsvLL[_.]([\\d_]+(?:[a-z])?)\\.zip"),

// --- Lumpinou (lumpinoumods.com / patreon) ---
new Rule("a1000005-lump-0001-0001-000000000001", "Lumpinou", "RPO Collection Version", Strategy.REGEX_TEXT, "RPO\\s+Collection\\s+v([\\d.]+)"),
new Rule("a1000005-lump-0001-0002-000000000001", "Lumpinou", "Lumpinou Patreon Version", Strategy.REGEX_TEXT, "(?:v|version\\s+)([\\d.]+)\\s*(?:\\(|\\[|-)"),

// --- SimRealist (itch.io) ---
new Rule("a1000006-simr-0001-0001-000000000001", "SimRealist", "SimRealist itch.io", Strategy.REGEX_TEXT, "SimRealist[_\\s]+[-_\\w]+[_\\s]+(\\d+\\.\\d+\\.\\d+\\.\\d+)\\.zip"),
new Rule("a1000006-simr-0001-0002-000000000001", "SimRealist", "SimRealist Page Version", Strategy.REGEX_TEXT, "Version\\s+(\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?)"),

// --- Generic (last resort) ---
new Rule("019b4232-c111-75b5-8b44-54943df3509f", "Generic", "vX.X.X", Strategy.REGEX_TEXT, "v?(\\d+\\.\\d+(?:\\.\\d+)?)")
);

rulesList.addAll(loadJson(RULES_FILE, mapper.getTypeFactory().constructCollectionType(List.class, Rule.class)));
}

Expand All @@ -123,7 +137,7 @@ private <T> List<T> loadJson(String fileName, JavaType javaType) {
try {
return mapper.readValue(file, javaType);
} catch (IOException e) {
e.printStackTrace();
LOG.log(Level.WARNING, "Failed to load " + fileName, e);
}
return new ArrayList<>();
}
Expand All @@ -148,14 +162,21 @@ public String saveImage(File originalImage) {
Files.copy(originalImage.toPath(), dest);
return dest.toString();
} catch (IOException e) {
e.printStackTrace();
LOG.log(Level.WARNING, "Failed to save image", e);
return null;
}
}

public String downloadAndSaveImage(String imageUrl) {
try {
URL url = new URI(imageUrl.trim()).toURL();
URI uri = new URI(imageUrl.trim());
String scheme = uri.getScheme();
if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
LOG.warning("Rejected non-HTTP image URL: " + imageUrl);
return null;
}

URL url = uri.toURL();
String fileName = "web_" + System.currentTimeMillis() + ".png";
Path dest = Paths.get(COVERS_DIR, fileName);

Expand All @@ -164,8 +185,12 @@ public String downloadAndSaveImage(String imageUrl) {
}
return dest.toString();
} catch (Exception e) {
e.printStackTrace();
LOG.log(Level.WARNING, "Failed to download image: " + imageUrl, e);
return null;
}
}

public static Path getDataDir() {
return DATA_DIR;
}
}
Loading