diff --git a/.github/assets/CURSEFORGE_inspect_url.png b/.github/assets/CURSEFORGE_inspect_url.png new file mode 100644 index 0000000..950c89c Binary files /dev/null and b/.github/assets/CURSEFORGE_inspect_url.png differ diff --git a/.github/assets/MODRINTH_versions_page.png b/.github/assets/MODRINTH_versions_page.png new file mode 100644 index 0000000..3cfdd2f Binary files /dev/null and b/.github/assets/MODRINTH_versions_page.png differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b01da52..42fb112 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,4 +34,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: Artifacts - path: build/libs/ \ No newline at end of file + path: | + fabric/build/libs/ + neoforge/build/libs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index d8619bb..797dda0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ replay_*.log /translators/input /translators/output +/fabric/runs/ diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..d9ca844 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,506 @@ +# Simple Mod Sync - User Guide + +**Table of contents** + +- [Simple Mod Sync - User Guide](#simple-mod-sync-user-guide) + - [Quick Start Guide](#quick-start-guide) + - [Step 1: Create Your Sync File](#step-1-create-your-sync-file) + - [Step 2: Add Your First Mod](#step-2-add-your-first-mod) + - [Step 3: Add More Content](#step-3-add-more-content) + - [Step 4: Share Your File](#step-4-share-your-file) + - [Understanding the Sync File](#understanding-the-sync-file) + - [The Basics](#the-basics) + - [Making Content Updatable](#making-content-updatable) + - [Different Types of Content](#different-types-of-content) + - [Mods](#mods) + - [Resource Packs](#resource-packs) + - [Shaders](#shaders) + - [Data Packs](#data-packs) + - [Config Files (Packed Content)](#config-files-packed-content) + - [Getting Download Links](#getting-download-links) + - [From Modrinth](#from-modrinth) + - [From CurseForge](#from-curseforge) + - [From Other Sources](#from-other-sources) + - [Complete Example](#complete-example) + - [Tips and Troubleshooting](#tips-and-troubleshooting) + - [Glossary](#glossary) + - [Advanced Features](#advanced-features) + - [File Modifications](#file-modifications) + - [Remove Files](#remove-files) + - [Rename/Move Files](#renamemove-files) + + +--- + +## Quick Start Guide + +### Step 1: Create Your Sync File + +Open any text editor (Notepad, TextEdit, etc.) and paste this template: + +```json +{ + "sync_version": 3, + "sync": [ + + ] +} +``` + +Save it as `modpack.json` (or any name ending in `.json`). + +**What this means:** +- `sync_version: 3` tells Simple Mod Sync which format you're using (3 is the newest) +- `sync: [ ]` is where you'll put your list of content to download + +### Step 2: Add Your First Mod + +Let's add Sodium as an example. Between the square brackets `[ ]`, add: + +```json +{ + "sync_version": 3, + "sync": [ + { + "url": "https://cdn.modrinth.com/data/AANobbMI/versions/EoNKHoLH/sodium-fabric-0.6.5%2Bmc1.21.1.jar", + "name": "Sodium", + "version": "0.6.5", + "type": "mod" + } + ] +} +``` + +> Note that the URL is only as example, look at [From Modrinth](#from-modrinth) to get +> your own URL. + +**What each part means:** +- `url` - Where to download the file from +- `name` - A friendly name so you know what this is +- `version` - Any text that helps you track which version this is +- `type` - What kind of content this is (mod, resourcepack, shader, etc.) + +### Step 3: Add More Content + +To add more items, put a comma `,` after the closing `}` and add another item: + +```json +{ + "sync_version": 3, + "sync": [ + { + "url": "https://cdn.modrinth.com/data/AANobbMI/versions/EoNKHoLH/sodium-fabric-0.6.5%2Bmc1.21.1.jar", + "name": "Sodium", + "version": "0.6.5", + "type": "mod" + }, + { + "url": "https://cdn.modrinth.com/data/P7dR8mSH/versions/4OZL6q6h/fabric-api-0.110.5%2B1.21.1.jar", + "name": "Fabric API", + "version": "0.110.5", + "type": "mod" + } + ] +} +``` + +**Important:** Don't forget the comma between items! The last item should NOT have a comma after it. + +### Step 4: Share Your File + +1. Upload your `.json` file to a sharing service: + - **Pastebin**: Go to pastebin.com, paste your file, save, and use the "raw" link + - **GitHub Gist**: Create a gist at gist.github.com and use the "raw" link + - **Your own website**: Upload it anywhere and link directly to the file + +2. Make sure the link shows **only the text** (no website design around it) + +3. Give this link to your friends - they'll paste it into Simple Mod Sync and everything downloads automatically! + +--- + +## Understanding the Sync File + +### The Basics + +Every item in your sync list needs at minimum: +- **url** - The download link + +Optional but recommended: +- **name** - Helps you remember what this is +- **version** - Helps track updates +- **type** - Where to put the file (defaults to "mod") + +### Making Content Updatable + +The `version` field is important for updates. When you want to update a mod: + +1. Change the `url` to the new version's download link +2. Change the `version` to something different (any text works) +3. Users running the sync will automatically get the new version + +**Example:** +```json +{ + "url": "https://example.com/sodium-0.5.0.jar", + "version": "0.5.0", + "name": "Sodium" +} +``` + +When you update: +```json +{ + "url": "https://example.com/sodium-0.6.0.jar", + "version": "0.6.0", + "name": "Sodium" +} +``` + +Simple Mod Sync sees the version changed and downloads the new file! + +--- + +## Different Types of Content + +### Mods +```json +{ + "url": "https://example.com/cool-mod.jar", + "name": "Cool Mod", + "version": "1.0", + "type": "mod" +} +``` +Goes into your `mods` folder. + +### Resource Packs +```json +{ + "url": "https://example.com/texture-pack.zip", + "name": "Awesome Textures", + "version": "2.1", + "type": "resourcepack" +} +``` +Goes into your `resourcepacks` folder. + +### Shaders +```json +{ + "url": "https://example.com/shader.zip", + "name": "Beautiful Shaders", + "version": "1.5", + "type": "shader" +} +``` +Goes into your `shaderpacks` folder. + +### Data Packs +```json +{ + "url": "https://example.com/datapack.zip", + "name": "Custom Worldgen", + "version": "3.0", + "type": "datapack" +} +``` +Goes into your world's `datapacks` folder. + +### Config Files (Packed Content) + +For custom configurations or files that need to go in specific folders: + +```json +{ + "url": "https://example.com/configs.zip", + "name": "Modpack Configs", + "version": "1.0", + "type": "packed", + "directory": "config" +} +``` + +This downloads a ZIP file and extracts it into the folder you specify with `directory`. + +**Common uses:** +- `"directory": "config"` - Extract into the config folder +- `"directory": "config/somemod"` - Extract into a specific mod's config folder +- `"directory": "."` - Extract into the game directory root + +You can also use `"type": "config"` instead of `"type": "packed"` - they work the same way. + +--- + +## Getting Download Links + +### From Modrinth + +![Modrinth versions page example](./.github/assets/MODRINTH_versions_page.png) + +1. Go to the mod/pack page on Modrinth +2. Click the **Versions** tab +3. Find the version you want +4. **Right-click** the download button +5. Select "Copy link address" or "Copy link" +6. Paste this URL into your sync file + +The URL should look like: +``` +https://cdn.modrinth.com/data/PROJECT/versions/VERSION/filename.jar +``` + +**Important:** Make sure it ends with `.jar` or `.zip` - if it doesn't, you copied the wrong link! + +### From CurseForge + +Due to the way how CurseForge works, its not as easy to get the raw file URL. Either do it +by inspecting the network when downloading the mod. + +![CurseForge inspect page](./.github/assets/CURSEFORGE_inspect_url.png) + +Or follow this reddit post: https://www.reddit.com/r/feedthebeast/comments/fffna3/comment/fjyceu8/ + +The URL should look like: +``` +https://mediafilez.forgecdn.net/files/XXXX/YYY/filename.jar +``` + +**Note:** Some CurseForge links may redirect or change. If you have issues, consider re-uploading the file to a more stable hosting service. + +### From Other Sources + +You can use any direct download link that: +- Points directly to a `.jar` or `.zip` file +- Doesn't require login or clicking through pages +- Is publicly accessible + +**Good sources:** +- GitHub releases (use the "raw" download links) +- Direct file hosting services +- Your own web server + +**Avoid:** +- Links that go to web pages (not the file itself) +- Download sites with ads/waiting timers +- Links that require accounts or authentication + +--- + +## Complete Example + +Here's a full sync file with different types of content: + +```json +{ + "sync_version": 3, + "sync": [ + { + "url": "https://cdn.modrinth.com/data/AANobbMI/versions/EoNKHoLH/sodium-fabric-0.6.5%2Bmc1.21.1.jar", + "name": "Sodium", + "version": "0.6.5", + "type": "mod" + }, + { + "url": "https://cdn.modrinth.com/data/P7dR8mSH/versions/4OZL6q6h/fabric-api-0.110.5%2B1.21.1.jar", + "name": "Fabric API", + "version": "0.110.5", + "type": "mod" + }, + { + "url": "https://cdn.modrinth.com/data/slufHzC2/versions/Sdg6a6Tc/texture-pack.zip", + "name": "Cool Textures", + "version": "3.0", + "type": "resourcepack" + }, + { + "url": "https://cdn.modrinth.com/data/BS9T99lD/versions/tAx0UOBX/shaders.zip", + "name": "Amazing Shaders", + "version": "0.12", + "type": "shader" + }, + { + "url": "https://cdn.modrinth.com/data/lWDHr9jE/versions/aLQ1otmd/worldgen-pack.zip", + "name": "Custom World Generation", + "version": "2.4", + "type": "datapack" + }, + { + "url": "https://example.com/modpack-configs.zip", + "name": "Modpack Configuration", + "version": "1.0", + "type": "config", + "directory": "config" + } + ] +} +``` + +--- + +## Tips and Troubleshooting + +**✓ Always test your sync file** before sharing it! Run it yourself to make sure everything downloads correctly. + +**✓ Keep version numbers updated** every time you change a URL. This ensures users get the new version. + +**✓ Use clear names** so you and others know what each item is without checking the URL. + +**✓ Check your commas!** Missing or extra commas are the most common mistake. Every item needs a comma after it except the last one. + +**✓ Use a JSON validator** if something isn't working. Search "JSON validator" online and paste your file to check for errors. + +**✗ Don't use shortened URLs** (like bit.ly) - use the full direct download link. + +**✗ Don't include spaces or special characters** in version numbers unless necessary. + +--- + +## Glossary + +**Sync File / Schema File** - The `.json` file that contains your list of mods and content to download. + +**JSON** - A file format for storing structured data. It's just text with specific formatting rules (like needing commas between items). + +**URL** - The web address where a file can be downloaded from. Should point directly to a `.jar` or `.zip` file. + +**Version** - A text label that helps track which version of a mod you're using. Change this when you update the URL. + +**Type** - Tells Simple Mod Sync where to put the downloaded file (mods folder, resourcepacks folder, etc.). + +**Packed Content** - A ZIP file that gets extracted into a specific folder you choose. + +**Directory** - A folder path where packed content should be extracted to. + +--- + +## Advanced Features + +**Note:** This section is for advanced users who want more control over their Minecraft instance. Beginners can skip this entirely - the basic sync features above are all you need! + +### File Modifications + +The `modify` section lets you automatically remove or rename files in the game instance. This is useful for cleaning up old files or managing configurations. + +**Structure:** +```json +{ + "sync_version": 3, + "sync": [ + // ... your content here ... + ], + "modify": [ + // ... modifications here ... + ] +} +``` + +Each modification needs: +- `type` - What operation to perform ("remove" or "rename") +- `pattern` - A regex pattern that matches files to modify +- `path` - Where to look for files (usually `"."` for the game directory) + +**Warning:** Regex patterns can be complex. If you're not familiar with regex, use the examples below and test carefully! + +### Remove Files + +Automatically delete files matching a pattern. + +**Example - Remove user cache:** +```json +{ + "sync_version": 3, + "modify": [ + { + "type": "remove", + "pattern": "^usercache\\.json$", + "path": "." + } + ] +} +``` + +This removes the `usercache.json` file from the game directory. + +**Example - Remove old mod versions:** + +This might be useful when you removed a mod from your modpack. + +```json +{ + "type": "remove", + "pattern": "^mods/oldmod-.*\\.jar$", + "path": "." +} +``` + +This removes any file in the mods folder starting with "oldmod-" and ending with ".jar". + +### Rename/Move Files + +Move or rename files automatically. + +**Example - Backup a file:** +```json +{ + "sync_version": 3, + "modify": [ + { + "type": "rename", + "pattern": "^usercache\\.json$", + "result": "usercache_backup.json", + "path": "." + } + ] +} +``` + +This renames `usercache.json` to `usercache_backup.json`. + +**Common Patterns:** + +| What you want | Pattern | +|---------------|---------| +| Exact filename | `^filename\\.txt$` | +| Any file starting with "old" | `^old.*$` | +| Any .log file | `^.*\\.log$` | +| Files in config folder | `^config/.*$` | + +**Important Notes:** +- Always use double backslashes `\\` before dots in filenames (e.g., `\\.json` not `.json`) +- Test your patterns carefully - they can match more files than you expect! +- The `result` in rename operations is the new filename or path +- You can test regex patterns at regex101.com before using them + +**Full Example with Modifications:** +```json +{ + "sync_version": 3, + "sync": [ + { + "url": "https://cdn.modrinth.com/data/AANobbMI/versions/EoNKHoLH/sodium-fabric-0.6.5%2Bmc1.21.1.jar", + "name": "Sodium", + "version": "0.6.5", + "type": "mod" + } + ], + "modify": [ + { + "type": "remove", + "pattern": "^usercache\\.json$", + "path": "." + }, + { + "type": "remove", + "pattern": "^logs/.*\\.log$", + "path": "." + } + ] +} +``` + +This sync file downloads Sodium and cleans up the user cache and old log files. + +--- + +Note: This file was *improved* by Claude Sonnet 4.5 for better readability diff --git a/README.md b/README.md index 5dac1b8..e5786ba 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,21 @@ # Simple Mod Sync -A lightweight mod for synchronizing game mods via a URL-based schema. +A simple way to share and sync mods, resource packs, shaders, and other Minecraft content with your friends or community. -## Features +## What is Simple Mod Sync? -- **Automated Mod Synchronization**: Synchronize mods from a specified URL on every game start. -- **Customizable Destination**: Choose where mods are downloaded on your system. -- **User-Friendly Setup**: Simple configuration with minimal setup required. +Simple Mod Sync lets you create a list of mods and other content that can be automatically downloaded and updated. Instead of telling your friends "download these 20 mods from different websites," you give them one link and this mod does the rest! -## Example schema file +Think of it like a shopping list, but for Minecraft content. You write down what you want (the URLs to download files), and Simple Mod Sync goes shopping for you. -The file retrieved from the URL must follow this structure: -```json -{ - "sync_version": 3, - "sync": [ - { - "url": "https://example.com/url/to/mod.jar", - "name": "some-mod_name", - "version": "2.8.1", - "type": "mod" - } - ] -} -``` -> Add more mods/files as needed to the content array. -> Additionally you can add 'modify' section to the file. +## Usage -## Simple Setup Guide +If you are just downloading this mod because somebody sent you, you probably +just need to download the mod, put it into the mods file and be gone. -1. **Create the JSON File**: Use the [example schema](#example-schema-file) to create your mod list file. - -2. **Host the File**: Upload the JSON file to an HTTP server. You can use services like [Pastebin](https://pastebin.com) for this. - -3. **Install the Mod**: Install the _Simple Mod Sync_ mod as usual. When the game starts for the first time, it will prompt you to enter the URL of your JSON file. - -4. **Monitor Synchronization**: Once the URL is set, the mod synchronization status will be visible in the top-left corner of the title screen. - -- To change the URL later, simply update the `download_url` setting in the config file or in the synced mods menu. +If you are the one hosting the server and/or managing the mods, look into the +[Docs file](./DOCS.md). ## For Developers diff --git a/TODO b/TODO new file mode 100644 index 0000000..fab55a9 --- /dev/null +++ b/TODO @@ -0,0 +1,30 @@ +DOCUMENTATION +- Sync version +- Sync + - Types + - Urls (how to obtain them) + - Versions + - Paths for packed type +- Modify + - Types + - Patterns +- For developers + - The system + - Handlers + - Contributing + + +COMMON + - [x] Modifications + - [x] Auto sync + - [ ] Minecraft version and loader check + - UI + - [x] Update list of contents when schema parsed + - [x] Content type icon + - [x] Init screen + - [ ] Restart game popup + - [w] Translations + - [x] Tooltips + +NEOFORGE + - [x] module diff --git a/build.gradle b/build.gradle index 2f6c710..7cede0c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,80 +1,4 @@ plugins { - id 'fabric-loom' version '1.10-SNAPSHOT' - id 'maven-publish' -} - -version = project.mod_version -group = project.maven_group - -base { - archivesName = project.archives_base_name -} - -repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. -} - -fabricApi { - configureDataGeneration() -} - -dependencies { - // To change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - // Fabric API. This is technically optional, but you probably want it anyway. - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - -} - -processResources { - inputs.property "version", project.version - - filesMatching("fabric.mod.json") { - expand "version": project.version - } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = 21 -} - -java { - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() - - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -jar { - from("LICENSE") { - rename { "${it}_${project.base.archivesName.get()}"} - } -} - -// configure the maven publication -publishing { - publications { - create("mavenJava", MavenPublication) { - artifactId = project.archives_base_name - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } + id 'fabric-loom' version '1.10-SNAPSHOT' apply false + id 'net.neoforged.moddev' version '2.0.62-beta' apply false } \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..1957c33 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'groovy-gradle-plugin' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle new file mode 100644 index 0000000..881c91e --- /dev/null +++ b/buildSrc/src/main/groovy/multiloader-common.gradle @@ -0,0 +1,128 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +base { + archivesName = "${mod_id}-${project.name}-${minecraft_version}" +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(java_version) + withSourcesJar() + withJavadocJar() +} + +repositories { + mavenCentral() + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = 'https://repo.spongepowered.org/repository/maven-public' + } + } + filter { includeGroupAndSubgroups('org.spongepowered') } + } + exclusiveContent { + forRepositories( + maven { + name = 'ParchmentMC' + url = 'https://maven.parchmentmc.org/' + }, + maven { + name = "NeoForge" + url = 'https://maven.neoforged.net/releases' + } + ) + filter { includeGroup('org.parchmentmc.data') } + } + maven { + name = 'BlameJared' + url = 'https://maven.blamejared.com' + } +} + +['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> + configurations."$variant".outgoing { + capability("$group:${project.name}:$version") + capability("$group:${base.archivesName.get()}:$version") + capability("$group:$mod_id-${project.name}-${minecraft_version}:$version") + capability("$group:$mod_id:$version") + } + publishing.publications.configureEach { + suppressPomMetadataWarningsFor(variant) + } +} + +sourcesJar { + from(rootProject.file('LICENSE')) { + rename { "${it}_${mod_name}" } + } +} + +jar { + from(rootProject.file('LICENSE')) { + rename { "${it}_${mod_name}" } + } + + manifest { + attributes([ + 'Specification-Title' : mod_name, + 'Specification-Vendor' : mod_author, + 'Specification-Version' : project.jar.archiveVersion, + 'Implementation-Title' : project.name, + 'Implementation-Version': project.jar.archiveVersion, + 'Implementation-Vendor' : mod_author, + 'Built-On-Minecraft' : minecraft_version + ]) + } +} + +processResources { + var expandProps = [ + 'version' : version, + 'group' : project.group, + 'minecraft_version' : minecraft_version, + 'minecraft_version_range' : minecraft_version_range, + 'fabric_version' : fabric_version, + 'fabric_loader_version' : fabric_loader_version, + 'mod_name' : mod_name, + 'mod_author' : mod_author, + 'mod_id' : mod_id, + 'license' : license, + 'description' : project.description, + 'neoforge_version' : neoforge_version, + 'neoforge_loader_version_range': neoforge_loader_version_range, + 'credits' : credits, + 'java_version' : java_version + ] + + var jsonExpandProps = expandProps.collectEntries { + key, value -> [(key): value instanceof String ? value.replace("\n", "\\\\n") : value] + } + + filesMatching(['META-INF/mods.toml', 'META-INF/neoforge.mods.toml']) { + expand expandProps + } + + filesMatching(['pack.mcmeta', 'fabric.mod.json', '*.mixins.json']) { + expand jsonExpandProps + } + + inputs.properties(expandProps) +} + +publishing { + publications { + register('mavenJava', MavenPublication) { + artifactId base.archivesName.get() + from components.java + } + } + repositories { + maven { + url System.getenv('local_maven_url') + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/multiloader-loader.gradle b/buildSrc/src/main/groovy/multiloader-loader.gradle new file mode 100644 index 0000000..1821919 --- /dev/null +++ b/buildSrc/src/main/groovy/multiloader-loader.gradle @@ -0,0 +1,44 @@ +plugins { + id 'multiloader-common' +} + +configurations { + commonJava{ + canBeResolved = true + } + commonResources{ + canBeResolved = true + } +} + +dependencies { + compileOnly(project(':common')) { + capabilities { + requireCapability "$group:$mod_id" + } + } + commonJava project(path: ':common', configuration: 'commonJava') + commonResources project(path: ':common', configuration: 'commonResources') +} + +tasks.named('compileJava', JavaCompile) { + dependsOn(configurations.commonJava) + source(configurations.commonJava) +} + +processResources { + dependsOn(configurations.commonResources) + from(configurations.commonResources) +} + +tasks.named('javadoc', Javadoc).configure { + dependsOn(configurations.commonJava) + source(configurations.commonJava) +} + +tasks.named('sourcesJar', Jar) { + dependsOn(configurations.commonJava) + from(configurations.commonJava) + dependsOn(configurations.commonResources) + from(configurations.commonResources) +} \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..98fd4df --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'multiloader-common' + id 'net.neoforged.moddev' +} + +neoForge { + neoFormVersion = neo_form_version + def at = file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.from(at.absolutePath) + } + parchment { + minecraftVersion = parchment_minecraft + mappingsVersion = parchment_version + } +} + +dependencies { + compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' + compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' + annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' +} + +configurations { + commonJava { + canBeResolved = false + canBeConsumed = true + } + commonResources { + canBeResolved = false + canBeConsumed = true + } +} + +artifacts { + commonJava sourceSets.main.java.sourceDirectories.singleFile + commonResources sourceSets.main.resources.sourceDirectories.singleFile +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/HandlerRegistry.java b/common/src/main/java/dev/oxydien/simpleModSync/HandlerRegistry.java new file mode 100644 index 0000000..e79b945 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/HandlerRegistry.java @@ -0,0 +1,55 @@ +package dev.oxydien.simpleModSync; + +import dev.oxydien.simpleModSync.content.ContentType; +import dev.oxydien.simpleModSync.content.ContentTypeUtils; +import dev.oxydien.simpleModSync.content.handler.ContentHandler; +import dev.oxydien.simpleModSync.content.handler.GameContentHandler; +import dev.oxydien.simpleModSync.content.handler.ModContentHandler; +import dev.oxydien.simpleModSync.content.handler.PackedContentHandler; +import dev.oxydien.simpleModSync.modification.handler.ModificationHandler; +import dev.oxydien.simpleModSync.modification.handler.RemoveModificationHandler; +import dev.oxydien.simpleModSync.modification.handler.RenameModificationHandler; + +import java.util.HashMap; + +public class HandlerRegistry { + private final HashMap> contentHandlers = new HashMap<>(); + private final HashMap> modificationHandlers = new HashMap<>(); + + public HandlerRegistry() {} + + public ContentHandler getContentHandler(String id) { + String normalized = id.trim().toLowerCase(); + return contentHandlers.get(normalized); + } + + public ContentHandler getContentHandler(ContentType id) { + return this.getContentHandler(ContentTypeUtils.ToString(id)); + } + + public ModificationHandler getModificationHandler(String id) { + String normalized = id.trim().toLowerCase(); + return modificationHandlers.get(normalized); + } + + public void registerContentHandler(String id, ContentHandler handler) { + contentHandlers.put(id, handler); + } + public void registerModificationHandler(String id, ModificationHandler handler) { + modificationHandlers.put(id, handler); + } + + public void init() { + this.contentHandlers.put("mod", new ModContentHandler()); + this.contentHandlers.put("resourcepack", new GameContentHandler("resourcepacks")); + this.contentHandlers.put("datapack", new GameContentHandler("datapacks")); + this.contentHandlers.put("shader", new GameContentHandler("shaderpacks")); + + ContentHandler packedContentHandler = new PackedContentHandler(); + this.contentHandlers.put("packed", packedContentHandler); + this.contentHandlers.put("config", packedContentHandler); + + this.modificationHandlers.put("remove", new RemoveModificationHandler()); + this.modificationHandlers.put("rename", new RenameModificationHandler()); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/SimpleModSync.java b/common/src/main/java/dev/oxydien/simpleModSync/SimpleModSync.java new file mode 100644 index 0000000..7cc1abb --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/SimpleModSync.java @@ -0,0 +1,74 @@ +package dev.oxydien.simpleModSync; + +import dev.oxydien.simpleModSync.config.Config; +import dev.oxydien.simpleModSync.content.SyncSchema; +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.workers.SyncWorker; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class SimpleModSync { + public static final String MOD_ID = "simplemodsync"; + + private static SimpleModSync INSTANCE; + + public static void init(SimpleModSync instance) { + INSTANCE = instance; + } + + public static SimpleModSync getInstance() { + return INSTANCE; + } + + public abstract Path getInstanceDir(); + + public HandlerRegistry Handlers = new HandlerRegistry(); + private final AtomicReference workerThread = new AtomicReference<>(); + + @Nullable + public SyncSchema syncSchema; + public SyncWorker syncWorker; + + public void onInitialize() { + Log.init(MOD_ID); + Log.debug("Initializing SimpleModSync"); + + Path configPath = this.getInstanceDir().resolve("config").resolve(MOD_ID + ".json"); + new Config(configPath); + + this.Handlers.init(); + + if (Config.instance.getAutoDownload()) { + this.start(); + } + } + + public void start() { + if (this.syncWorker != null && this.syncWorker.isRunning()) { + return; + } + + Log.info("Starting SimpleModSync background worker thread..."); + + this.syncSchema = new SyncSchema(); + this.syncWorker = new SyncWorker(this.syncSchema); + + Thread thread = new Thread(this.syncWorker); + this.workerThread.set(thread); + thread.start(); + } + + public void stop() { // warn: unused + if (this.syncWorker != null) { + this.syncWorker.shutdown(); + this.syncWorker = null; + } + + Thread thread = workerThread.get(); + if (thread != null) { + thread.interrupt(); + } + } +} diff --git a/src/main/java/dev/oxydien/config/Config.java b/common/src/main/java/dev/oxydien/simpleModSync/config/Config.java similarity index 61% rename from src/main/java/dev/oxydien/config/Config.java rename to common/src/main/java/dev/oxydien/simpleModSync/config/Config.java index 3d91419..a3de5f5 100644 --- a/src/main/java/dev/oxydien/config/Config.java +++ b/common/src/main/java/dev/oxydien/simpleModSync/config/Config.java @@ -1,34 +1,34 @@ -package dev.oxydien.config; +package dev.oxydien.simpleModSync.config; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import dev.oxydien.logger.Log; -import org.jetbrains.annotations.Nullable; +import dev.oxydien.simpleModSync.log.Log; import java.io.*; +import java.nio.file.Path; public class Config { - private final String path; + private final Path path; private boolean autoDownload; private String downloadUrl; - private String downloadDestination; public static Config instance; - public Config(String path, @Nullable String downloadDestination) { - this.path = path; + public Config(Path configFilePath) { + this.path = configFilePath; this.autoDownload = true; this.downloadUrl = ""; - this.downloadDestination = downloadDestination; + this.load(); this.save(); + instance = this; - Log.Log.debug("Config file loaded"); + Log.debug("Config file loaded"); } - public String getPath() { + public Path getPath() { return this.path; } @@ -36,12 +36,13 @@ public boolean getAutoDownload() { return this.autoDownload; } - public String getDownloadUrl() { - return this.downloadUrl; + public void setAutoDownload(boolean autoDownload) { + this.autoDownload = autoDownload; + this.save(); } - public String getDownloadDestination(String type) { - return this.getDownloadDestination() + "/" + type; + public String getDownloadUrl() { + return this.downloadUrl; } public void setDownloadUrl(String downloadUrl) { @@ -49,26 +50,21 @@ public void setDownloadUrl(String downloadUrl) { this.save(); } - public String getDownloadDestination() { - return this.downloadDestination; - } - // Deserialize from json file public void load() { // Read from json file StringBuilder content = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new FileReader(this.getPath()))) { + try (BufferedReader br = new BufferedReader(new FileReader(this.getPath().toFile()))) { String line; while ((line = br.readLine()) != null) { content.append(line); } } catch (FileNotFoundException e) { - Log.Log.warn("config.load", "Config file not found, creating a default one", e); + Log.warning("config.load", "Config file not found, creating a default one", e); return; } catch (IOException e) { - Log.Log.error("config.load.IOException", "Failed to read config file", e); - return; + Log.error("config.load.IOException", "Failed to read config file", e); } // Parse json @@ -81,11 +77,6 @@ public void load() { if (downloadUrl != null && !downloadUrl.getAsString().isEmpty()) { this.downloadUrl = downloadUrl.getAsString(); } - - var downloadDestination = jsonElement.getAsJsonObject().get("download_destination"); - if (downloadDestination != null && downloadDestination.getAsString().isEmpty()) { - this.downloadDestination = downloadDestination.getAsString(); - } } // Serialize to json file @@ -94,13 +85,12 @@ public void save() { JsonObject json = new JsonObject(); json.addProperty("auto_download", this.autoDownload); json.addProperty("download_url", this.downloadUrl); - json.addProperty("download_destination", this.downloadDestination); // Write to json file - try (BufferedWriter bw = new BufferedWriter(new FileWriter(this.getPath()))) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(this.getPath().toFile()))) { bw.write(json.toString()); } catch (IOException e) { - Log.Log.error("config.save.IOException", "Failed to write config file", e); + Log.error("config.save.IOException", "Failed to write config file", e); } } -} +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/Content.java b/common/src/main/java/dev/oxydien/simpleModSync/content/Content.java new file mode 100644 index 0000000..e8e2ad4 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/Content.java @@ -0,0 +1,31 @@ +package dev.oxydien.simpleModSync.content; + +public class Content { + private final String uri; + private final ContentType type; + private final String name; + private final String version; + + public Content(String uri, ContentType type, String name, String version) { + this.uri = uri; + this.type = type; + this.name = name; + this.version = version; + } + + public String getUri() { + return uri; + } + + public ContentType getType() { + return type; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/ContentInfo.java b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentInfo.java new file mode 100644 index 0000000..d617e55 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentInfo.java @@ -0,0 +1,21 @@ +package dev.oxydien.simpleModSync.content; + +public class ContentInfo { + private final T content; + + public ContentInfo(T content) { + this.content = content; + } + + public String GetTitle() { + return this.content.getName().substring(0, Math.min(22, this.content.getName().length())); + } + + public T GetContent() { + return this.content; + } + + public ContentType GetContentType() { + return this.content.getType(); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/ContentType.java b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentType.java new file mode 100644 index 0000000..cef9407 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentType.java @@ -0,0 +1,10 @@ +package dev.oxydien.simpleModSync.content; + +public enum ContentType { + Mod, + ResourcePack, + ShaderPack, + DataPack, + Packed, + Config, // Same as Packed +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/ContentTypeUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentTypeUtils.java new file mode 100644 index 0000000..579733a --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/ContentTypeUtils.java @@ -0,0 +1,23 @@ +package dev.oxydien.simpleModSync.content; + +public class ContentTypeUtils { + public static String ToString(ContentType type) { + return switch (type) { + case Mod -> "mod"; + case ResourcePack -> "resourcepack"; + case ShaderPack -> "shader"; + case DataPack -> "datapack"; + case Packed, Config -> "packed"; + }; + } + + public static ContentType FromString(String type) { + return switch (type) { + case "resourcepack" -> ContentType.ResourcePack; + case "datapack" -> ContentType.DataPack; + case "shader" -> ContentType.ShaderPack; + case "config", "packed" -> ContentType.Packed; + default -> ContentType.Mod; + }; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/PackedContent.java b/common/src/main/java/dev/oxydien/simpleModSync/content/PackedContent.java new file mode 100644 index 0000000..ca85196 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/PackedContent.java @@ -0,0 +1,18 @@ +package dev.oxydien.simpleModSync.content; + +public class PackedContent extends Content{ + private String directory = ""; + + public PackedContent(String uri, ContentType type, String name, String version) { + super(uri, type, name, version); + } + + public PackedContent(String uri, ContentType type, String name, String version, String directory) { + super(uri, type, name, version); + this.directory = directory; + } + + public String getDirectory() { + return this.directory; + } +} diff --git a/src/main/java/dev/oxydien/data/ConfigData.java b/common/src/main/java/dev/oxydien/simpleModSync/content/PackedContentMetadata.java similarity index 68% rename from src/main/java/dev/oxydien/data/ConfigData.java rename to common/src/main/java/dev/oxydien/simpleModSync/content/PackedContentMetadata.java index f204cec..b18bb6c 100644 --- a/src/main/java/dev/oxydien/data/ConfigData.java +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/PackedContentMetadata.java @@ -1,15 +1,14 @@ -package dev.oxydien.data; +package dev.oxydien.simpleModSync.content; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import java.util.List; import java.util.stream.Collectors; -/** - * Used to save information about config content (NOT CONFIG OF THIS MOD) - * Theoretically, can be used as a bundle not just for config - */ -public class ConfigData { +public class PackedContentMetadata { public static final int CURRENT_SCHEME_VERSION = 1; public static final List ALLOWED_SCHEME_VERSIONS = List.of(1); @@ -17,15 +16,15 @@ public class ConfigData { /// List of modified files (absolute path) private final List config; - public ConfigData(List config) { + public PackedContentMetadata(List config) { this.schemeVersion = CURRENT_SCHEME_VERSION; this.config = config; } - public static ConfigData fromJson(JsonObject jsonObject) { + public static PackedContentMetadata FromJson(JsonObject jsonObject) { int schemeVersion = jsonObject.get("scheme_version").getAsInt(); if (!ALLOWED_SCHEME_VERSIONS.contains(schemeVersion)) { - throw new IllegalArgumentException("Invalid scheme version: " + schemeVersion); + throw new UnsupportedOperationException("Invalid scheme version: " + schemeVersion); } List config = jsonObject.get("config").getAsJsonArray() @@ -33,23 +32,31 @@ public static ConfigData fromJson(JsonObject jsonObject) { .stream() .map(JsonElement::getAsString) .collect(Collectors.toList()); - return new ConfigData(config); + return new PackedContentMetadata(config); } public JsonObject toJson() { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("scheme_version", schemeVersion); + JsonArray configArray = new JsonArray(); for (String configItem : config) { configArray.add(configItem); } + jsonObject.add("config", configArray); return jsonObject; } + public String toJsonString() { + Gson gson = new Gson(); + return gson.toJson(toJson()); + } + public int getSchemeVersion() { return schemeVersion; } + /// List of modified files (absolute path) public List getConfig() { return config; diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/SyncSchema.java b/common/src/main/java/dev/oxydien/simpleModSync/content/SyncSchema.java new file mode 100644 index 0000000..55419b5 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/SyncSchema.java @@ -0,0 +1,101 @@ +package dev.oxydien.simpleModSync.content; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import dev.oxydien.simpleModSync.exception.JsonValidationException; +import dev.oxydien.simpleModSync.modification.Modification; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.*; + +public class SyncSchema { + public static int CURRENT_VERSION = 3; + public static int[] SUPPORTED_VERSIONS = new int[] { 2, 3 }; + + private final ConcurrentHashMap contents = new ConcurrentHashMap<>(); + private final ConcurrentHashMap modifications = new ConcurrentHashMap<>(); + private final ConcurrentHashMap progress = new ConcurrentHashMap<>(); + + public SyncWork ParseJson(JsonObject rootObject) { + if (!rootObject.has("sync_version")) { + throw new JsonValidationException("sync_version", "Integer"); + } + + int syncVersion = rootObject.get("sync_version").getAsInt(); + if (Arrays.stream(SUPPORTED_VERSIONS).noneMatch((val) -> val == syncVersion)) { + throw new UnsupportedOperationException(String.format("Invalid sync_version: %d", syncVersion)); + } + + List contentIndexesToCheck = new ArrayList<>(); + + JsonArray syncArray = new JsonArray(); + if (rootObject.has("sync") && rootObject.get("sync").isJsonArray()) { + syncArray = rootObject.getAsJsonArray("sync"); + } + + this.progress.clear(); + + for (int i = 0; i < syncArray.size(); i++) { + if (!syncArray.get(i).isJsonObject()) { + this.progress.put(i, SyncStatus.OfState(SyncStatus.SyncState.INVALID)); + continue; + } + + this.progress.put(i, new SyncStatus()); + contentIndexesToCheck.add(i); + } + + List modificationIndexesToCheck = new ArrayList<>(); + + JsonArray modificationArray = new JsonArray(); + if (rootObject.has("modify") && rootObject.get("modify").isJsonArray()) { + modificationArray = rootObject.getAsJsonArray("modify"); + } + + for (int i = 0; i < modificationArray.size(); i++) { + if (!modificationArray.get(i).isJsonObject()) { + continue; + } + + modificationIndexesToCheck.add(i); + } + + return new SyncWork(contentIndexesToCheck, modificationIndexesToCheck); + } + + public interface UpdateStatusHandler { + void UpdateStatus(SyncStatus status); + } + + public void withStatus(int index, UpdateStatusHandler handler) { + if (!this.progress.containsKey(index)) { + return; + } + + // Just to be sure :3 + SyncStatus status = this.progress.get(index); + handler.UpdateStatus(status); + this.progress.put(index, status); + } + + public void setContent(int index, Content content) { + this.contents.put(index, content); + } + + public ConcurrentHashMap getProgress() { + return progress; + } + + public ConcurrentHashMap getContents() { + return contents; + } + + public ConcurrentHashMap getModifications() { + return modifications; + } + + public record SyncWork(List contentsToCheck, List modificationsToExecute) { + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/SyncStatus.java b/common/src/main/java/dev/oxydien/simpleModSync/content/SyncStatus.java new file mode 100644 index 0000000..40f961d --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/SyncStatus.java @@ -0,0 +1,86 @@ +package dev.oxydien.simpleModSync.content; + +import net.minecraft.network.chat.Component; + +public class SyncStatus { + public enum SyncState { + STARTING, + PARSING, + FINISHED, + DOWNLOADING, + MODIFIED, + UNSYNCED, + RETRIEVING_SCHEMA, + INVALID, + UNSUPPORTED, + ERROR, + } + + public static Component TranslatedState(SyncState state) { + return switch (state) { + case STARTING -> Component.translatable("simple_mod_sync.ui.sync_state.starting"); + case PARSING -> Component.translatable("simple_mod_sync.ui.sync_state.parsing"); + case FINISHED -> Component.translatable("simple_mod_sync.ui.sync_state.finished"); + case DOWNLOADING -> Component.translatable("simple_mod_sync.ui.sync_state.downloading"); + case MODIFIED -> Component.translatable("simple_mod_sync.ui.sync_state.modified"); + case UNSYNCED -> Component.translatable("simple_mod_sync.ui.sync_state.unsynced"); + case RETRIEVING_SCHEMA -> Component.translatable("simple_mod_sync.ui.sync_state.retrieving_schema"); + case INVALID -> Component.translatable("simple_mod_sync.ui.sync_state.invalid"); + case UNSUPPORTED -> Component.translatable("simple_mod_sync.ui.sync_state.unsupported"); + case ERROR -> Component.translatable("simple_mod_sync.ui.sync_state.error"); + }; + } + + private SyncState syncState; + private float downloadProgress; // 0 -> 1 + private String errorMessage; + + public SyncStatus() { + this.syncState = SyncState.STARTING; + this.downloadProgress = 0; + this.errorMessage = ""; + } + + public static SyncStatus OfState(SyncState state) { + SyncStatus status = new SyncStatus(); + status.setState(state); + return status; + } + + public static SyncStatus OfError(String errorMessage) { + SyncStatus status = new SyncStatus(); + status.setErrorMessage(errorMessage); + return status; + } + + public SyncState getState() { + if (!this.errorMessage.isBlank()) { + return SyncState.ERROR; + } + return this.syncState; + } + + public void setState(SyncState state) { + this.syncState = state; + } + + public float getDownloadProgress() { + return downloadProgress; + } + + public void setDownloadProgress(float downloadProgress) { + this.downloadProgress = downloadProgress; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public boolean isError() { + return this.getState() == SyncState.ERROR; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ContentHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ContentHandler.java new file mode 100644 index 0000000..813ee27 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ContentHandler.java @@ -0,0 +1,96 @@ +package dev.oxydien.simpleModSync.content.handler; + +import com.google.gson.JsonObject; +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.*; +import dev.oxydien.simpleModSync.exception.JsonValidationException; +import dev.oxydien.simpleModSync.io.FileOperations; +import dev.oxydien.simpleModSync.utils.DirUtils; +import dev.oxydien.simpleModSync.utils.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class ContentHandler { + public Content ParseJson(JsonObject contentObject, int index) { + if (!contentObject.has("url") + && !contentObject.has("uri")) { + throw new JsonValidationException("url", "String"); + } + + String url = (contentObject.has("url") ? contentObject.get("url") : contentObject.get("uri")).getAsString(); + + String name = contentObject.has("name") + ? contentObject.get("name").getAsString() + : String.format("UNNAMED_%d", index); + + String typeStr = contentObject.has("type") ? contentObject.get("type").getAsString() : "mod"; + ContentType type = ContentTypeUtils.FromString(typeStr); + + String version = contentObject.has("version") ? contentObject.get("version").getAsString() : "0"; + + return new Content(url, type, name, version); + } + + public boolean CheckExistence(T contentObject) { + Path dir = this.GetDirectory(SimpleModSync.getInstance().getInstanceDir()); + + Path filePath = dir.resolve(this.GetFileName(contentObject)); + return Files.exists(filePath); + } + + @Nullable + public Path GetOlderVersion(T contentObject) { + Path dir = this.GetDirectory(SimpleModSync.getInstance().getInstanceDir()); + String projectName = this.GetProjectName(contentObject); + + return DirUtils.DirContains(dir, projectName, false); + } + + public String GetFileName(T contentObject) { + return String.format("%s-%s.%s", StringUtils.sanitize(contentObject.getName()), StringUtils.sanitize(contentObject.getVersion()), this.GetFileExtension()); + } + + public String GetProjectName(T contentObject) { + return String.format("%s-", StringUtils.sanitize(contentObject.getName())); + } + + public ContentInfo GetInfo(T contentObject) { + return new ContentInfo<>(contentObject); + } + + public boolean NeedsUpdate(T contentObject) { + return !this.CheckExistence(contentObject); + } + + public void UpdateVersion(T contentObject, FileOperations files, int index) { + Path dir = this.GetDirectory(SimpleModSync.getInstance().getInstanceDir()); + String fileName = this.GetFileName(contentObject); + + Path outputPath = dir.resolve(fileName); + + files.DownloadFromUri(contentObject.getUri(), outputPath, index); + } + + /// 0 -> 1 + public float GetProgress(T contentObject, SyncStatus status) { + SyncStatus.SyncState state = status.getState(); + if (state == SyncStatus.SyncState.STARTING || state == SyncStatus.SyncState.UNSYNCED) { + return 0; + } + if (state == SyncStatus.SyncState.FINISHED || state == SyncStatus.SyncState.MODIFIED) { + return 1f; + } + if (state == SyncStatus.SyncState.PARSING) { + return 0.03f; + } + + return status.getDownloadProgress() * 0.9f + 0.1f; + } + + public abstract String GetFileExtension(); + + public abstract Path GetDirectory(Path basePath); + +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/handler/GameContentHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/GameContentHandler.java new file mode 100644 index 0000000..cd82ece --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/GameContentHandler.java @@ -0,0 +1,31 @@ +package dev.oxydien.simpleModSync.content.handler; + +import dev.oxydien.simpleModSync.content.Content; + +import java.nio.file.Path; + +public class GameContentHandler extends ContentHandler { + private final String extension; + private final String directory; + + public GameContentHandler(String extension, String directory) { + this.extension = extension; + this.directory = directory; + } + + public GameContentHandler(String directory) { + this.extension = "zip"; + this.directory = directory; + } + + + @Override + public String GetFileExtension() { + return this.extension; + } + + @Override + public Path GetDirectory(Path basePath) { + return basePath.resolve(this.directory); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ModContentHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ModContentHandler.java new file mode 100644 index 0000000..ee46578 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/ModContentHandler.java @@ -0,0 +1,17 @@ +package dev.oxydien.simpleModSync.content.handler; + +import dev.oxydien.simpleModSync.content.Content; + +import java.nio.file.Path; + +public class ModContentHandler extends ContentHandler { + @Override + public String GetFileExtension() { + return "jar"; + } + + @Override + public Path GetDirectory(Path basePath) { + return basePath.resolve("mods"); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/content/handler/PackedContentHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/PackedContentHandler.java new file mode 100644 index 0000000..41b1ce2 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/content/handler/PackedContentHandler.java @@ -0,0 +1,97 @@ +package dev.oxydien.simpleModSync.content.handler; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.Content; +import dev.oxydien.simpleModSync.content.PackedContent; +import dev.oxydien.simpleModSync.content.PackedContentMetadata; +import dev.oxydien.simpleModSync.io.FileOperations; +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.utils.StringUtils; +import dev.oxydien.simpleModSync.utils.ZipUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class PackedContentHandler extends ContentHandler { + @Override + public PackedContent ParseJson(JsonObject contentObject, int index) { + Content baseContent = super.ParseJson(contentObject, index); + + String directory = contentObject.has("directory") ? contentObject.get("directory").getAsString() : ""; + + return new PackedContent(baseContent.getUri(), baseContent.getType(), baseContent.getName(), baseContent.getVersion(), directory); + } + + @Override + public String GetFileExtension() { + return "json"; + } + + @Override + public String GetFileName(PackedContent contentObject) { + return String.format("sms_%s-%s.%s", StringUtils.sanitize(contentObject.getName()), StringUtils.sanitize(contentObject.getVersion()), this.GetFileExtension()); + } + + @Override + public String GetProjectName(PackedContent contentObject) { + return String.format("sms_%s-", contentObject.getName()); + } + + @Override + public Path GetDirectory(Path basePath) { + return basePath; + } + + @Override + public void UpdateVersion(PackedContent contentObject, FileOperations files, int index) { + Path dir = this.GetDirectory(SimpleModSync.getInstance().getInstanceDir()); + String safeName = StringUtils.sanitize(contentObject.getName()); + String safeVersion = StringUtils.sanitize(contentObject.getVersion()); + + Path metadataPath = dir.resolve(this.GetFileName(contentObject)); + Path tempZipPath = dir.resolve(String.format("sms_%s-%s.archive.zip", safeName, safeVersion)); + + + // Remove older version if exists + Path olderVersion = this.GetOlderVersion(contentObject); + if (olderVersion != null) { + Log.debug("UpdateVersion.PackedContentHandler", "Found older version of {}, deleting {}", safeName, olderVersion.getFileName()); + try { + String configJsonStr = Files.readString(olderVersion); + JsonObject jsonObject = JsonParser.parseString(configJsonStr).getAsJsonObject(); + PackedContentMetadata configData = PackedContentMetadata.FromJson(jsonObject); + + for (String file : configData.getConfig()) { + try { + Files.deleteIfExists(Path.of(file)); + } catch (IOException e) { + Log.warning("UpdateVersion.PackedContentHandler.delete", "Failed to delete file, ignoring: {}", e.getMessage()); + } + } + + Files.deleteIfExists(olderVersion); + } catch (IOException e) { + Log.error("UpdateVersion.PackedContentHandler.cleanup", "Failed to read or delete old version", e); + } + } + + // Download new version + try { + Log.debug("UpdateVersion.PackedContentHandler", "Downloading {} {}", contentObject.getName(), contentObject.getVersion()); + files.DownloadFromUri(contentObject.getUri(), tempZipPath, index); + + List modifiedFiles = ZipUtils.ExtractZipFile(tempZipPath, dir.resolve(StringUtils.sanitizeDirectory(contentObject.getDirectory()))); + List modifiedFilesAsString = modifiedFiles.stream().map(Path::toString).toList(); + PackedContentMetadata metadata = new PackedContentMetadata(modifiedFilesAsString); + Files.writeString(metadataPath, metadata.toJsonString()); + + Files.deleteIfExists(tempZipPath); + } catch (IOException e) { + Log.error("UpdateVersion.PackedContentHandler.download", "Failed to download or write file {}", contentObject.getName(), e); + } + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/exception/JsonValidationException.java b/common/src/main/java/dev/oxydien/simpleModSync/exception/JsonValidationException.java new file mode 100644 index 0000000..7f47d9f --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/exception/JsonValidationException.java @@ -0,0 +1,9 @@ +package dev.oxydien.simpleModSync.exception; + +public class JsonValidationException extends RuntimeException { + + public JsonValidationException(String fieldName, String expectedType) { + super(String.format("Required field '%s' of type %s is missing from JSON", + fieldName, expectedType)); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/io/FileOperations.java b/common/src/main/java/dev/oxydien/simpleModSync/io/FileOperations.java new file mode 100644 index 0000000..03c201b --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/io/FileOperations.java @@ -0,0 +1,82 @@ +package dev.oxydien.simpleModSync.io; + +import dev.oxydien.simpleModSync.content.SyncSchema; +import dev.oxydien.simpleModSync.content.SyncStatus; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileOperations { + private final SyncSchema syncSchema; + + public FileOperations(SyncSchema schema) { + this.syncSchema = schema; + } + + public interface ProgressCallback { + void onProgress(int percentage); // 0 -> 100 + } + + public void DownloadFromUri(String uri, Path output, int index) { + try { + this.downloadFileWithProgress(uri, output, (newProgress) -> { + syncSchema.withStatus(index, (status -> { + status.setState(SyncStatus.SyncState.DOWNLOADING); + status.setDownloadProgress(newProgress / (float) 100); + })); + }); + } catch (Exception e) { + syncSchema.withStatus(index, (status -> { + status.setErrorMessage(e.getMessage()); + })); + } + } + + public void downloadFileWithProgress(String uriString, Path outputPath, ProgressCallback callback) throws IOException, URISyntaxException { + URL url = new URI(uriString).toURL(); + URLConnection connection = url.openConnection(); + + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.setRequestMethod("GET"); + } + + long fileSize = connection.getContentLengthLong(); + InputStream inputStream = connection.getInputStream(); + + OutputStream outputStream = Files.newOutputStream(outputPath); + + byte[] buffer = new byte[4096]; + int bytesRead; + long totalBytesRead = 0; + int lastReportedProgress = -1; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + totalBytesRead += bytesRead; + + if (fileSize > 0) { + int progress = (int) ((totalBytesRead * 100) / fileSize); + if (progress > lastReportedProgress) { + callback.onProgress(progress); + lastReportedProgress = progress; + } + } + } + + outputStream.close(); + inputStream.close(); + + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); + } + + if (lastReportedProgress < 100) { + callback.onProgress(100); + } + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/log/Log.java b/common/src/main/java/dev/oxydien/simpleModSync/log/Log.java new file mode 100644 index 0000000..8868479 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/log/Log.java @@ -0,0 +1,100 @@ +package dev.oxydien.simpleModSync.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Log { + private static Log instance; + private static Logger logger; + private static String modId; + + // Minimum log level - change this to control what gets logged + private static final LogLevel MIN_LEVEL = LogLevel.ALL; + + private Log(String modId) { + Log.modId = modId; + logger = LoggerFactory.getLogger(modId); + } + + /** + * Initialize the logger with your mod ID + * Call this once in your mod's initialization + */ + public static void init(String modId) { + if (instance == null) { + instance = new Log(modId); + } + } + + /** + * Log a debug message + */ + public static void debug(Object... messages) { + if (MIN_LEVEL.ordinal() <= LogLevel.DEBUG.ordinal()) { + logger.info(formatMessage(messages)); + } + } + + /** + * Log an info message + */ + public static void info(Object... messages) { + if (MIN_LEVEL.ordinal() <= LogLevel.INFO.ordinal()) { + logger.info(formatMessage(messages)); + } + } + + /** + * Log a warning message + */ + public static void warning(Object... messages) { + if (MIN_LEVEL.ordinal() <= LogLevel.WARNING.ordinal()) { + logger.warn(formatMessage(messages)); + } + } + + /** + * Log an error message + */ + public static void error(Object... messages) { + if (MIN_LEVEL.ordinal() <= LogLevel.ERROR.ordinal()) { + logger.error(formatMessage(messages)); + } + } + + /** + * Log an error message with an exception + */ + public static void error(Throwable throwable, Object... messages) { + if (MIN_LEVEL.ordinal() <= LogLevel.ERROR.ordinal()) { + logger.error(formatMessage(messages), throwable); + } + } + + private static String formatMessage(Object... messages) { + if (messages == null || messages.length == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(Log.modId); + sb.append("] "); + + for (int i = 0; i < messages.length; i++) { + sb.append(messages[i]); + if (i < messages.length - 1) { + sb.append(" "); + } + } + return sb.toString(); + } + + private enum LogLevel { + ALL, + DEBUG, + INFO, + WARNING, + ERROR + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/mixin/TitleScreenMixin.java b/common/src/main/java/dev/oxydien/simpleModSync/mixin/TitleScreenMixin.java new file mode 100644 index 0000000..3e4320f --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/mixin/TitleScreenMixin.java @@ -0,0 +1,46 @@ +package dev.oxydien.simpleModSync.mixin; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.config.Config; +import dev.oxydien.simpleModSync.ui.ProgressHelper; +import dev.oxydien.simpleModSync.ui.screens.InitScreen; +import dev.oxydien.simpleModSync.ui.widgets.TotalSyncProgress; +import dev.oxydien.simpleModSync.ui.widgets.TotalSyncStatus; +import dev.oxydien.simpleModSync.workers.SyncWorker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TitleScreen.class) +public class TitleScreenMixin extends Screen { + protected TitleScreenMixin(Component title) { + super(title); + } + + @Inject(at = @At("HEAD"), method = "init", cancellable = true) + private void simple_mod_sync$initHead(CallbackInfo ci) { + if (Config.instance.getDownloadUrl().isEmpty()) { + Minecraft.getInstance().setScreen(new InitScreen()); + ci.cancel(); + } + } + + @Inject(at = @At("RETURN"), method = "init") + private void simple_mod_sync$init(CallbackInfo ci) { + ProgressHelper progressHelper = new ProgressHelper(SimpleModSync.getInstance()); + + final int heightOffset = 3; + + TotalSyncProgress barWidget = new TotalSyncProgress(0, 0, this.width, heightOffset, progressHelper); + this.addRenderableOnly(barWidget); + + SyncWorker worker = SimpleModSync.getInstance().syncWorker; + + this.addRenderableWidget(new TotalSyncStatus(0, heightOffset, worker, progressHelper)); + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/Modification.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/Modification.java new file mode 100644 index 0000000..f3e3539 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/Modification.java @@ -0,0 +1,25 @@ +package dev.oxydien.simpleModSync.modification; + +public class Modification { + private final ModificationType type; + private final String pattern; + private final String path; + + public Modification(ModificationType type, String pattern, String path) { + this.type = type; + this.pattern = pattern; + this.path = path; + } + + public ModificationType getType() { + return this.type; + } + + public String getPattern() { + return this.pattern; + } + + public String getPath() { + return this.path; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationType.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationType.java new file mode 100644 index 0000000..de5c5a4 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationType.java @@ -0,0 +1,7 @@ +package dev.oxydien.simpleModSync.modification; + +public enum ModificationType { + Remove, + Rename, + Unknown // ignored +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationTypeUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationTypeUtils.java new file mode 100644 index 0000000..89322d4 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/ModificationTypeUtils.java @@ -0,0 +1,21 @@ +package dev.oxydien.simpleModSync.modification; + +import dev.oxydien.simpleModSync.content.ContentType; + +public class ModificationTypeUtils { + public static String ToString(ModificationType type) { + return switch (type) { + case Remove -> "remove"; + case Rename -> "rename"; + case Unknown -> "unknown"; + }; + } + + public static ModificationType FromString(String type) { + return switch (type) { + case "rename" -> ModificationType.Rename; + case "remove" -> ModificationType.Remove; + default -> ModificationType.Unknown; + }; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/RenameModification.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/RenameModification.java new file mode 100644 index 0000000..b69f7f9 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/RenameModification.java @@ -0,0 +1,14 @@ +package dev.oxydien.simpleModSync.modification; + +public class RenameModification extends Modification { + private final String result; + + public RenameModification(ModificationType type, String pattern, String path, String result) { + super(type, pattern, path); + this.result = result; + } + + public String getResult() { + return result; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/ModificationHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/ModificationHandler.java new file mode 100644 index 0000000..901e9e6 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/ModificationHandler.java @@ -0,0 +1,84 @@ +package dev.oxydien.simpleModSync.modification.handler; + +import com.google.gson.JsonObject; +import dev.oxydien.simpleModSync.content.Content; +import dev.oxydien.simpleModSync.content.ContentType; +import dev.oxydien.simpleModSync.content.ContentTypeUtils; +import dev.oxydien.simpleModSync.exception.JsonValidationException; +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.modification.Modification; +import dev.oxydien.simpleModSync.modification.ModificationType; +import dev.oxydien.simpleModSync.modification.ModificationTypeUtils; +import dev.oxydien.simpleModSync.utils.DirUtils; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public abstract class ModificationHandler { + public Modification ParseJson(JsonObject contentObject) { + if (!contentObject.has("type")) { + throw new JsonValidationException("type", "String (remove | rename)"); + } + + if (!contentObject.has("pattern")) { + throw new JsonValidationException("pattern", "String"); + } + + String typeStr = contentObject.get("type").getAsString(); + ModificationType type = ModificationTypeUtils.FromString(typeStr); + + String pattern = contentObject.get("pattern").getAsString(); + + String path = contentObject.has("path") ? contentObject.get("path").getAsString() : "."; + + return new Modification(type, pattern, path); + } + + public List GetRelevantPaths(T mod, Path basePath) { + Path workingDir = this.GetWorkingDirectory(mod, basePath); + + return DirUtils.GetFilePaths(workingDir); + } + + public Path GetWorkingDirectory(T mod, Path basePath) { + return DirUtils.sanitizePath(basePath, mod.getPath()); + } + + public Pattern GetPattern(T mod) { + return Pattern.compile(mod.getPattern()); + } + + public abstract void ApplyOn(T mod, Path filePath) throws IOException; + + public void Execute(T mod, Path basePath) throws Exception { + List relevantPaths = this.GetRelevantPaths(mod, basePath); + Log.debug("Running mod", mod.getPattern(), "in", mod.getPath(), "on", relevantPaths.size(), "possible items"); + + List sanitized = new ArrayList<>(); + for (Path path : relevantPaths) { + Path absolute = path.toAbsolutePath(); + String relativePath = basePath.relativize(absolute).toString(); + sanitized.add(relativePath); + } + + List matches = new ArrayList<>(); + Pattern pattern = this.GetPattern(mod); + + for (var filePath : sanitized) { + var matcher = pattern.matcher(filePath); + if (matcher.matches()) { + matches.add(filePath); + Log.debug("Execute.ModificationHandler", "Found match for {} at {}", mod.getPattern(), filePath); + break; + } + } + + for (var match : matches) { + Path filePath = basePath.resolve(match); + this.ApplyOn(mod, filePath); + } + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RemoveModificationHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RemoveModificationHandler.java new file mode 100644 index 0000000..7273d10 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RemoveModificationHandler.java @@ -0,0 +1,14 @@ +package dev.oxydien.simpleModSync.modification.handler; + +import dev.oxydien.simpleModSync.modification.Modification; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class RemoveModificationHandler extends ModificationHandler { + @Override + public void ApplyOn(Modification mod, Path filePath) throws IOException { + Files.delete(filePath); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RenameModificationHandler.java b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RenameModificationHandler.java new file mode 100644 index 0000000..e6d3aca --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/modification/handler/RenameModificationHandler.java @@ -0,0 +1,32 @@ +package dev.oxydien.simpleModSync.modification.handler; + +import com.google.gson.JsonObject; +import dev.oxydien.simpleModSync.exception.JsonValidationException; +import dev.oxydien.simpleModSync.modification.Modification; +import dev.oxydien.simpleModSync.modification.RenameModification; +import dev.oxydien.simpleModSync.utils.StringUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class RenameModificationHandler extends ModificationHandler { + @Override + public RenameModification ParseJson(JsonObject contentObject) { + if (!contentObject.has("result")) { + throw new JsonValidationException("result", "String"); + } + + Modification base = super.ParseJson(contentObject); + + String result = contentObject.get("result").getAsString(); + + return new RenameModification(base.getType(), base.getPattern(), base.getPath(), result); + } + + @Override + public void ApplyOn(RenameModification mod, Path filePath) throws IOException { + Path parent = filePath.getParent(); + Files.move(filePath, parent.resolve(StringUtils.sanitizeDirectory(mod.getResult()))); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/ProgressHelper.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/ProgressHelper.java new file mode 100644 index 0000000..63e5033 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/ProgressHelper.java @@ -0,0 +1,116 @@ +package dev.oxydien.simpleModSync.ui; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.Content; +import dev.oxydien.simpleModSync.content.ContentType; +import dev.oxydien.simpleModSync.content.SyncStatus; +import dev.oxydien.simpleModSync.content.handler.ContentHandler; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; + +import java.util.Iterator; + +public class ProgressHelper { + private static final ResourceLocation LOADING_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/loading.png"); + private static final ResourceLocation FINISHED_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/finished.png"); + private static final ResourceLocation PARSING_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/parsing.png"); + private static final ResourceLocation UNSYNCED_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/unsynced.png"); + private static final ResourceLocation ERROR_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/error.png"); + private static final ResourceLocation MODIFIED_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/modified.png"); + private static final ResourceLocation PREPARING_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/preparing.png"); + private static final ResourceLocation DOWNLOADING_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/downloading.png"); + + private static final ResourceLocation MOD_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/mod.png"); + private static final ResourceLocation RESOURCEPACK_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/resourcepack.png"); + private static final ResourceLocation SHADER_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/shader.png"); + private static final ResourceLocation DATAPACK_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/datapack.png"); + private static final ResourceLocation PACKED_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/packed.png"); + private static final ResourceLocation UNKNOWN_ICON = ResourceLocation.tryBuild(SimpleModSync.MOD_ID, "ui/textures/icons/unknown.png"); + + private final SimpleModSync plugin; + private int frameCall = 0; + + public ProgressHelper(SimpleModSync instance) { + this.plugin = instance; + } + + public float getOverallProgress() { + if (plugin == null || plugin.syncSchema == null) { + return 0; + } + + int allElements = 0; + float progressSum = 0; + + var present = plugin.syncSchema.getProgress(); + for (Iterator it = present.keys().asIterator(); it.hasNext(); ) { + int key = it.next(); + + Content content = plugin.syncSchema.getContents().get(key); + if (content == null) { + continue; + } + + SyncStatus progress = plugin.syncSchema.getProgress().get(key); + + ContentHandler handler = plugin.Handlers.getContentHandler(content.getType()); + + if (handler == null) { + continue; + } + + ++allElements; + float contentProgress = handler.GetProgress(content, progress); + progressSum += contentProgress; + } + + return progressSum / allElements; + } + + public void drawStatusIcon(SyncStatus syncStatus, GuiGraphics guiGraphics, int x, int y) { + this.drawStatusIcon(syncStatus, guiGraphics, x, y, 3); + } + + public void drawStatusIcon(SyncStatus syncStatus, GuiGraphics guiGraphics, int x, int y, int margin) { + guiGraphics.blit(RenderType::guiTextured, this.getStateIcon(syncStatus), x + margin, y + margin, 0, 0, 10, 10, 10, 10); + } + + public void drawContentTypeIcon(ContentType type, GuiGraphics guiGraphics, int x, int y, int margin) { + guiGraphics.blit(RenderType::guiTextured, this.getContentTypeIcon(type), x + margin, y + margin, 0, 0, 10, 10, 10, 10); + } + + public void drawLoadingIcon(GuiGraphics guiGraphics, int x, int y) { + ++this.frameCall; + int frameOffset = (int) (double) (this.frameCall / 6); + if (frameOffset >= 8) { + this.frameCall = 0; + frameOffset = 0; + } + + guiGraphics.blit(RenderType::guiTextured, LOADING_ICON, x, y, 0, 16 * frameOffset, 16, 16, 16, 128); + } + + private ResourceLocation getStateIcon(SyncStatus syncStatus) { + return switch (syncStatus.getState()) { + case UNSYNCED -> UNSYNCED_ICON; + case FINISHED -> FINISHED_ICON; + case STARTING -> PREPARING_ICON; + case PARSING -> PARSING_ICON; + case DOWNLOADING -> DOWNLOADING_ICON; + case MODIFIED -> MODIFIED_ICON; + default -> ERROR_ICON; + }; + } + + private ResourceLocation getContentTypeIcon(ContentType type) { + return switch (type) { + case ContentType.Mod -> MOD_ICON; + case ResourcePack -> RESOURCEPACK_ICON; + case ShaderPack -> SHADER_ICON; + case DataPack -> DATAPACK_ICON; + case Packed, Config -> PACKED_ICON; + default -> UNKNOWN_ICON; + }; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/ContentSyncScreen.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/ContentSyncScreen.java new file mode 100644 index 0000000..86361d9 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/ContentSyncScreen.java @@ -0,0 +1,327 @@ +package dev.oxydien.simpleModSync.ui.screens; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.config.Config; +import dev.oxydien.simpleModSync.content.SyncSchema; +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.ui.ProgressHelper; +import dev.oxydien.simpleModSync.ui.widgets.ContentProgressWidget; +import dev.oxydien.simpleModSync.ui.widgets.TotalSyncProgress; +import dev.oxydien.simpleModSync.workers.SyncWorker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.MultiLineTextWidget; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ContentSyncScreen extends Screen { + private static final int WIDGET_GAP = 4; + private static final int CONTENT_WIDTH = 300; + + private SyncSchema schema; + private SyncWorker worker; + private final Screen parent; + private ScrollableContentList contentList; + private final ProgressHelper progressHelper; + private String errorMessage; + + public ContentSyncScreen(Component title, @Nullable Screen parent) { + super(title); + this.schema = SimpleModSync.getInstance().syncSchema; + this.worker = SimpleModSync.getInstance().syncWorker; + this.parent = parent; + this.progressHelper = new ProgressHelper(SimpleModSync.getInstance()); + this.errorMessage = ""; + } + + @Override + protected void init() { + super.init(); + final int heightOffset = 3; + + // Progress bar + TotalSyncProgress barWidget = new TotalSyncProgress(0, 0, this.width, heightOffset, this.progressHelper); + this.addRenderableOnly(barWidget); + + // Back button + this.addRenderableWidget(new Button.Builder(Component.translatable("simple_mod_sync.ui.content_screen.back_button"), + (buttonWidget) -> Minecraft.getInstance().setScreen(this.parent)).pos(3, 5).size(60, 20).build()); + + // Title + Component titleText = Component.translatable("simple_mod_sync.ui.content_screen.title"); + this.addRenderableOnly( + new MultiLineTextWidget(this.width / 2 - titleText.getString().length() - 30, 10, titleText, this.font) + .setColor(0xFF3DF6B4)); + + // Url field + EditBox urlField = new EditBox(this.font, this.width / 2 - 150, 24, + 300, 20, Component.literal("")); + urlField.setMaxLength(368); + urlField.setValue(Config.instance.getDownloadUrl()); + this.addRenderableWidget(urlField); + + // Save Url button + this.addRenderableWidget(new Button.Builder(Component.translatable("simple_mod_sync.ui.content_screen.save_url_button"), (buttonWidget) -> { + String url = urlField.getValue(); + Config.instance.setDownloadUrl(url); + }).pos(this.width / 2 - 150, 45).size(95, 20).build()); + + // Sync button + this.addRenderableWidget(new Button.Builder(Component.translatable("simple_mod_sync.ui.content_screen.sync_button"), + (buttonWidget) -> this.startSync()).pos(this.width / 2 - 48, 45).size(95, 20).build()); + + // Auto download toggle button widget + AtomicBoolean autoDownload = new AtomicBoolean(Config.instance.getAutoDownload()); + Component autoDownloadTextTrue = Component.translatable("simple_mod_sync.ui.content_screen.auto_download_true"); + Component autoDownloadTextFalse = Component.translatable("simple_mod_sync.ui.content_screen.auto_download_false"); + Button auto_download = new Button.Builder(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse, (buttonWidget) -> { + autoDownload.set(!autoDownload.get()); + Config.instance.setAutoDownload(autoDownload.get()); + buttonWidget.setMessage(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse); + }).pos(this.width / 2 + 55, 45).size(95, 20).build(); + this.addRenderableWidget(auto_download); + + int contentLeft = this.width / 2 - 150; + + // Initialize scrollable content list + int listTop = 80; + int listBottom = this.height - 5; + + this.contentList = new ScrollableContentList( + CONTENT_WIDTH, + listBottom - listTop, + listTop, + contentLeft + ); + this.addWidget(this.contentList); + + this.initContent(); + } + + private void initContent() { + if (this.schema == null || this.contentList == null) return; + this.contentList.clean(false); + + var progress = this.schema.getProgress(); + + for (var iterator = progress.keys().asIterator(); iterator.hasNext();) { + int key = iterator.next(); + + ContentProgressWidget widget = new ContentProgressWidget( + 0, 0, CONTENT_WIDTH, this.font, this.progressHelper, this.schema, key + ); + this.contentList.addEntry(widget); + } + } + + private void updateState() { + this.initContent(); + + if (this.worker != null) + if (this.worker.getStatus().isError()) { + this.errorMessage = this.worker.getStatus().getErrorMessage(); + } else { + this.errorMessage = ""; + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + if (this.worker == null && SimpleModSync.getInstance().syncWorker != null) { + this.schema = SimpleModSync.getInstance().syncSchema; + this.worker = SimpleModSync.getInstance().syncWorker; + this.worker.subscribeUpdateCallback(this::updateState); + } + + if (this.contentList != null) { + this.contentList.render(guiGraphics, mouseX, mouseY, partialTick); + } + + if (this.worker != null && this.worker.getStatus().isError()) { + guiGraphics.drawString(this.font, this.worker.getStatus().getErrorMessage(), this.width / 2 - 150, 65, 0xFFFF1C1C, false); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.contentList != null && this.contentList.mouseScrolled(mouseX, mouseY, scrollX, scrollY)) { + return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + + private void startSync() { + SimpleModSync.getInstance().start(); + this.worker = null; + this.schema = null; + } + + // Inner class for scrollable list + private static class ScrollableContentList extends AbstractContainerEventHandler implements Renderable, GuiEventListener, NarratableEntry { + private final List entries = new ArrayList<>(); + private final int width; + private final int height; + private final int top; + private final int left; + private double scrollAmount = 0; + private boolean scrolling = false; + + public ScrollableContentList(int width, int height, int top, int left) { + this.width = width; + this.height = height; + this.top = top; + this.left = left; + } + + public void addEntry(ContentProgressWidget widget) { + this.entries.add(widget); + this.updatePositions(); + } + + public void clean(boolean updatePositions) { + this.entries.clear(); + if (updatePositions) + this.updatePositions(); + } + + private void updatePositions() { + int yPos = 0; + for (ContentProgressWidget widget : this.entries) { + widget.setX(this.left); + widget.setY(this.top + yPos - (int) this.scrollAmount); + yPos += widget.getHeight() + WIDGET_GAP; + } + } + + private int getContentHeight() { + int total = 0; + for (ContentProgressWidget widget : this.entries) { + total += widget.getHeight() + WIDGET_GAP; + } + return Math.max(0, total - WIDGET_GAP); + } + + private int getMaxScroll() { + return Math.max(0, this.getContentHeight() - this.height); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + // Update positions in case heights changed + this.updatePositions(); + + // Enable scissor for clipping + guiGraphics.enableScissor(this.left, this.top, this.left + this.width, this.top + this.height); + + for (ContentProgressWidget widget : this.entries) { + if (this.isWidgetVisible(widget)) { + widget.render(guiGraphics, mouseX, mouseY, partialTick); + } + } + + guiGraphics.disableScissor(); + + // Draw scrollbar if needed + if (this.getMaxScroll() > 0) { + this.renderScrollbar(guiGraphics); + } + } + + private boolean isWidgetVisible(ContentProgressWidget widget) { + int widgetTop = widget.getY(); + int widgetBottom = widgetTop + widget.getHeight(); + return widgetBottom >= this.top && widgetTop <= this.top + this.height; + } + + private void renderScrollbar(GuiGraphics guiGraphics) { + int scrollbarX = this.left + this.width + 2; + int scrollbarWidth = 6; + + // Scrollbar background + guiGraphics.fill(scrollbarX, this.top, scrollbarX + scrollbarWidth, + this.top + this.height, 0xFF000000); + + // Scrollbar thumb + int maxScroll = this.getMaxScroll(); + int thumbHeight = Math.max(20, (int) ((float) this.height / this.getContentHeight() * this.height)); + int thumbY = this.top + (int) ((this.scrollAmount / maxScroll) * (this.height - thumbHeight)); + + guiGraphics.fill(scrollbarX, thumbY, scrollbarX + scrollbarWidth, + thumbY + thumbHeight, 0xFF808080); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.isMouseOver(mouseX, mouseY)) { + this.scrollAmount = Mth.clamp(this.scrollAmount - scrollY * 10, 0, this.getMaxScroll()); + return true; + } + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (this.scrolling) { + this.scrollAmount = Mth.clamp(this.scrollAmount - dragY, 0, this.getMaxScroll()); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.scrolling = button == 0 && this.isMouseOverScrollbar(mouseX, mouseY); + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0) { + this.scrolling = false; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseX >= this.left && mouseX <= this.left + this.width && + mouseY >= this.top && mouseY <= this.top + this.height; + } + + private boolean isMouseOverScrollbar(double mouseX, double mouseY) { + int scrollbarX = this.left + this.width + 2; + return mouseX >= scrollbarX && mouseX <= scrollbarX + 6 && + mouseY >= this.top && mouseY <= this.top + this.height; + } + + @Override + public List children() { + return this.entries; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) { + + } + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/InitScreen.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/InitScreen.java new file mode 100644 index 0000000..d8d1515 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/screens/InitScreen.java @@ -0,0 +1,153 @@ +package dev.oxydien.simpleModSync.ui.screens; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.config.Config; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.Component; + +public class InitScreen extends Screen { + private static final int CONTENT_WIDTH = 300; + private static final int SPACING = 10; + private static final int TEXT_FIELD_HEIGHT = 20; + private static final int BUTTON_HEIGHT = 20; + + private EditBox urlField; + private Checkbox autoUpdateCheckbox; + private int contentStartY; + + public InitScreen() { + super(Component.translatable("simple_mod_sync.ui.init_screen.title")); + } + + @Override + protected void init() { + super.init(); + + int totalHeight = calculateTotalHeight(); + contentStartY = (this.height - totalHeight) / 2; + + int centerX = this.width / 2; + int currentY = contentStartY; + + // Skip title + currentY += 20 + SPACING; + + // Skip sub-header + currentY += 10 + SPACING; + + // Skip disclaimer (3 lines) + currentY += 30 + SPACING * 2; + + // URL text field + urlField = new EditBox(this.font, centerX - CONTENT_WIDTH / 2, currentY, + CONTENT_WIDTH, TEXT_FIELD_HEIGHT, Component.literal("URL")); + urlField.setHint(Component.literal("https://example.com/sync.json")); + urlField.setMaxLength(500); + + // Load current URL if exists + String currentUrl = Config.instance.getDownloadUrl(); + if (currentUrl != null && !currentUrl.isEmpty()) { + urlField.setValue(currentUrl); + } + + this.addRenderableWidget(urlField); + currentY += TEXT_FIELD_HEIGHT + SPACING * 2; + + // Auto-update checkbox + autoUpdateCheckbox = Checkbox.builder( + Component.translatable("simple_mod_sync.ui.init_screen.auto_update"), + this.font) + .pos(centerX - CONTENT_WIDTH / 2, currentY) + .selected(Config.instance.getAutoDownload()) + .build(); + + this.addRenderableWidget(autoUpdateCheckbox); + currentY += BUTTON_HEIGHT + SPACING * 2; + + // Save button + Button saveButton = Button.builder( + Component.translatable("simple_mod_sync.ui.init_screen.save"), + button -> this.onSave()) + .bounds(centerX - CONTENT_WIDTH / 2, currentY, CONTENT_WIDTH, BUTTON_HEIGHT) + .build(); + + this.addRenderableWidget(saveButton); + } + + private int calculateTotalHeight() { + int height = 0; + height += 20; // Title + height += SPACING; + height += 10; // Sub-header + height += SPACING; + height += 30; // Disclaimer (3 lines) + height += SPACING * 2; + height += TEXT_FIELD_HEIGHT; // URL field + height += SPACING * 2; + height += BUTTON_HEIGHT; // Checkbox + height += SPACING * 2; + height += BUTTON_HEIGHT; // Save button + return height; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + + int centerX = this.width / 2; + int currentY = contentStartY; + + // Title + guiGraphics.drawCenteredString(this.font, this.title, centerX, currentY, 0xFFFFFF); + currentY += 20 + SPACING; + + // Sub-header + Component subHeader = Component.translatable("simple_mod_sync.ui.init_screen.sub_header"); + guiGraphics.drawCenteredString(this.font, subHeader, centerX, currentY, 0xAAAAAA); + currentY += 10 + SPACING; + + // Disclaimer + String disclaimer = "§7By entering a URL below, you acknowledge that you are"; + String disclaimer2 = "§7responsible for any content synced from that source."; + String disclaimer3 = "§7Use trusted sources only."; + + guiGraphics.drawCenteredString(this.font, disclaimer, centerX, currentY, 0xFFFFFF); + currentY += 10; + guiGraphics.drawCenteredString(this.font, disclaimer2, centerX, currentY, 0xFFFFFF); + currentY += 10; + guiGraphics.drawCenteredString(this.font, disclaimer3, centerX, currentY, 0xFFFFFF); + } + + private void onSave() { + String url = urlField.getValue().trim(); + + // Validate URL + if (url.isEmpty()) { + Config.instance.setDownloadUrl("-"); + } else { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + urlField.setTextColor(0xFF5555); + return; + } + Config.instance.setDownloadUrl(url); + } + + Config.instance.setAutoDownload(autoUpdateCheckbox.selected()); + + if (autoUpdateCheckbox.selected()) { + SimpleModSync.getInstance().start(); + } + + this.minecraft.setScreen(new TitleScreen()); + } + + @Override + public void onClose() { + this.minecraft.setScreen(new TitleScreen()); + } +} \ No newline at end of file diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/ContentProgressWidget.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/ContentProgressWidget.java new file mode 100644 index 0000000..91d433b --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/ContentProgressWidget.java @@ -0,0 +1,127 @@ +package dev.oxydien.simpleModSync.ui.widgets; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.*; +import dev.oxydien.simpleModSync.content.handler.ContentHandler; +import dev.oxydien.simpleModSync.ui.ProgressHelper; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +public class ContentProgressWidget extends AbstractWidget { + private static final int WIDGET_DEFAULT_HEIGHT = 28; + private static final int DEFAULT_PADDING = 4; + + private final Font font; + private final ProgressHelper helper; + + private final int index; + private final SyncSchema schema; + private ContentInfo contentInfo; + private ContentHandler handler; + private SyncStatus status; + + public ContentProgressWidget(int x, int y, int width, Font font, ProgressHelper helper, SyncSchema schema, int index) { + super(x, y, width, WIDGET_DEFAULT_HEIGHT, Component.empty()); + + this.font = font; + this.helper = helper; + + this.index = index; + this.schema = schema; + + this.setTooltipDelay(Duration.of(500, ChronoUnit.MILLIS)); + + this.fetchData(); + } + + private void fetchData() { + if (schema.getProgress().containsKey(this.index)) { + this.status = schema.getProgress().get(index); + } + + if (schema.getContents().containsKey(index)) { + Content content = schema.getContents().get(index); + this.handler = SimpleModSync.getInstance().Handlers.getContentHandler(content.getType()); + this.contentInfo = this.handler.GetInfo(content); + } + + this.updateTooltip(); + } + + private void updateTooltip() { + var builder = Component.empty(); + if (this.contentInfo != null) { + builder.append(Component.translatable(String.format("simple_mod_sync.ui.content_type.%s", ContentTypeUtils.ToString(this.contentInfo.GetContentType())))); + builder.append(" "); + } + builder.append(this.getModName()); + builder.append(": "); + if (this.status != null) { + builder.append(SyncStatus.TranslatedState(this.status.getState())); + } + this.setTooltip(Tooltip.create(builder)); + } + + @Override + public int getHeight() { + int base = super.getHeight(); + if (this.status != null && this.status.getState() == SyncStatus.SyncState.ERROR) { + return base + this.font.lineHeight + DEFAULT_PADDING; + } + return base; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int i, int i1, float v) { + this.fetchData(); // This might not be the most optimized way + + guiGraphics.fill(RenderType.guiOverlay(), this.getX(), this.getY(), this.getX() + this.getWidth(), + this.getY() + this.getHeight(), 0xa0000000); + + if (this.status != null) { + // Progress bar + if (contentInfo != null) { + guiGraphics.fill( + this.getX(), + this.getY(), + (int) (this.getX() + (((float) this.width) * this.handler.GetProgress(this.contentInfo.GetContent(), this.status))), + this.getY() + 3, + 0xFFFFFFFF + ); + } + + // Status icon + this.helper.drawStatusIcon(this.status, guiGraphics, this.getX() + DEFAULT_PADDING, this.getY() + DEFAULT_PADDING + 2, 0); + + if (this.status.getState() == SyncStatus.SyncState.ERROR) { + String msg = this.status.getErrorMessage(); + guiGraphics.drawString(this.font, String.format("§c§l%s§r", msg), this.getX() + DEFAULT_PADDING, + this.getY() + DEFAULT_PADDING * 2 + this.font.lineHeight, 0xFF55FFFF, false); + } + } + + if (this.contentInfo != null) { + this.helper.drawContentTypeIcon(this.contentInfo.GetContentType(), guiGraphics, + this.getX() + DEFAULT_PADDING, this.getY() + DEFAULT_PADDING + 12, 0); + } + + guiGraphics.drawString(this.font, String.format("§l%s§r", this.getModName()), + this.getX() + 20 + DEFAULT_PADDING, this.getY() + DEFAULT_PADDING + this.font.lineHeight / 2, 0xFF55FFFF, false); + } + + private String getModName() { + return this.contentInfo == null ? this.index + " ???" : this.contentInfo.GetTitle(); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/LinearScrollLayout.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/LinearScrollLayout.java new file mode 100644 index 0000000..9883f30 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/LinearScrollLayout.java @@ -0,0 +1,100 @@ +package dev.oxydien.simpleModSync.ui.widgets; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractScrollArea; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.Layout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.network.chat.CommonComponents; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class LinearScrollLayout extends AbstractScrollArea { + private final List children = new ArrayList<>(); + private final Layout layout; + + public LinearScrollLayout(int width, int height) { + super(0, 0, width, height, CommonComponents.EMPTY); + this.layout = new LinearLayout(width, height, LinearLayout.Orientation.VERTICAL); + layout.visitWidgets(this::addWidget); + } + + public void addWidget(AbstractWidget widget) { + this.children.add(widget); + } + + protected int contentHeight() { + return this.layout.getHeight(); + } + + protected double scrollRate() { + return 10.0F; + } + + protected void renderWidget(GuiGraphics gui, int mouseX, int mouseY, float delta) { + gui.enableScissor(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height); + gui.pose().pushPose(); + gui.pose().translate(0.0F, -this.scrollAmount(), 0.0F); + + for(AbstractWidget abstractwidget : this.children) { + abstractwidget.render(gui, mouseX, mouseY, delta); + } + + gui.pose().popPose(); + gui.disableScissor(); + this.renderScrollbar(gui); + } + + protected void updateWidgetNarration(@NotNull NarrationElementOutput narrationElementOutput) { + } + + public @NotNull ScreenRectangle getBorderForArrowNavigation(@NotNull ScreenDirection screenDirection) { + return new ScreenRectangle(this.getX(), this.getY(), this.width, this.contentHeight()); + } + + public void setFocused(@Nullable GuiEventListener eventListener) { + super.setFocused(false); + if (eventListener != null) { + ScreenRectangle screenrectangle = this.getRectangle(); + ScreenRectangle eventRectangle = eventListener.getRectangle(); + int i = (int)((double)eventRectangle.top() - this.scrollAmount() - (double)screenrectangle.top()); + int j = (int)((double)eventRectangle.bottom() - this.scrollAmount() - (double)screenrectangle.bottom()); + if (i < 0) { + this.setScrollAmount(this.scrollAmount() + (double)i - (double)14.0F); + } else if (j > 0) { + this.setScrollAmount(this.scrollAmount() + (double)j + (double)14.0F); + } + } + + } + + public List children() { + return this.children; + } + + public void setX(int x) { + super.setX(x); + this.layout.setX(x); + this.layout.arrangeElements(); + } + + public void setY(int y) { + super.setY(y); + this.layout.setY(y); + this.layout.arrangeElements(); + } + + public @NotNull Collection getNarratables() { + return this.children; + } + +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncProgress.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncProgress.java new file mode 100644 index 0000000..4d397f2 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncProgress.java @@ -0,0 +1,28 @@ +package dev.oxydien.simpleModSync.ui.widgets; + +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.ui.ProgressHelper; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; + +public class TotalSyncProgress extends AbstractWidget { + private final ProgressHelper progressHelper; + + public TotalSyncProgress(int x, int y, int width, int height, ProgressHelper progressHelper) { + super(x, y, width, height, Component.empty()); + this.progressHelper = progressHelper; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int i, int i1, float v) { + float progress = this.progressHelper.getOverallProgress(); + + guiGraphics.fill(0, 0, (int) (((float) this.width) * progress), this.height, 0xAFFFFFFF); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncStatus.java b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncStatus.java new file mode 100644 index 0000000..505228e --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/ui/widgets/TotalSyncStatus.java @@ -0,0 +1,95 @@ +package dev.oxydien.simpleModSync.ui.widgets; + +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.SyncStatus; +import dev.oxydien.simpleModSync.ui.ProgressHelper; +import dev.oxydien.simpleModSync.ui.screens.ContentSyncScreen; +import dev.oxydien.simpleModSync.workers.SyncWorker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +public final class TotalSyncStatus extends AbstractWidget { + private static final int SIZE = 16; + + private final int x; + private final int y; + private final ProgressHelper progressHelper; + private SyncWorker syncWorker; + private boolean isFocused; + + private static final ResourceLocation BACKGROUND = ResourceLocation.withDefaultNamespace("widget/button_disabled"); + + public TotalSyncStatus(int x, int y, SyncWorker syncWorker, + ProgressHelper progressHelper) { + super(x, y, SIZE, SIZE, Component.empty()); + this.x = x; + this.y = y; + this.syncWorker = syncWorker; + this.progressHelper = progressHelper; + this.setTooltip(Tooltip.create(SyncStatus.TranslatedState(SyncStatus.SyncState.UNSYNCED))); + this.setTooltipDelay(Duration.of(200, ChronoUnit.MILLIS)); + } + + @Override + public void renderWidget(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float v) { + if (this.syncWorker == null) { + this.syncWorker = SimpleModSync.getInstance().syncWorker; + } + + if (this.isFocused() || mouseX < this.x + SIZE && mouseX > this.x && mouseY < this.y + SIZE && mouseY > this.y) + { + guiGraphics.blitSprite(RenderType::guiTexturedOverlay, BACKGROUND, this.x, this.y, SIZE, SIZE); + } + + if (syncWorker == null) { + this.progressHelper.drawStatusIcon(SyncStatus.OfState(SyncStatus.SyncState.UNSYNCED), guiGraphics, this.x, this.y); + return; + } + + this.progressHelper.drawStatusIcon(this.syncWorker.getStatus(), guiGraphics, this.x, this.y); + + if (this.syncWorker.isRunning()) { + this.progressHelper.drawLoadingIcon(guiGraphics, this.x, this.y); + } + + this.setTooltip(Tooltip.create(SyncStatus.TranslatedState(this.syncWorker.getStatus().getState()))); + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + Minecraft.getInstance().setScreen(new ContentSyncScreen(Component.translatable("simple_mod_sync.ui.sync_full_view.title"), null)); + } + + @Override + public void setFocused(boolean b) { + this.isFocused = b; + } + + @Override + public boolean isFocused() { + return this.isFocused; + } + + @Override + public @NotNull NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + narrationElementOutput.add(NarratedElementType.HINT, "Sync status"); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/utils/DirUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/utils/DirUtils.java new file mode 100644 index 0000000..23ef539 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/utils/DirUtils.java @@ -0,0 +1,97 @@ +package dev.oxydien.simpleModSync.utils; + +import dev.oxydien.simpleModSync.log.Log; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class DirUtils { + /** + * Checks if any file in given directory matches the filename. If fullname is false it checks if any file starts with given filename. + * @param path The directory for the file to be checked + * @param filename The (partial) filename + * @param fullname Whether the filename is whole or not + * @return The path if found or null if not found + */ + @Nullable + public static Path DirContains(Path path, String filename, boolean fullname) { + try (var stream = Files.list(path)) { + for (var dirFile : stream.toList()) { + if (fullname) { + if (dirFile.getFileName().toString().equals(filename)) { + return dirFile; + } + else { + continue; + } + } + + if (dirFile.getFileName().toString().startsWith(filename)) { + return dirFile; + } + } + } catch (IOException e) { + Log.warning("Failed to list contents of " + path, e); + return null; + } + return null; + } + + /** + * Returns a list of all file paths under the given directory, recursively. + * @param startPath The root directory path to start searching from + * @return List of absolute file paths as strings + */ + public static List GetFilePaths(Path startPath) { + List filePaths = new ArrayList<>(); + + if (!startPath.toFile().exists() || !startPath.toFile().isDirectory()) { + throw new IllegalArgumentException("Invalid directory path: " + startPath); + } + + collectFilePaths(startPath, filePaths); + return filePaths; + } + + private static void collectFilePaths(Path directory, List filePaths) { + File[] files = directory.toFile().listFiles(); + if (files != null) { + for (File file : files) { + filePaths.add(file.toPath()); + if (file.isDirectory()) { + collectFilePaths(file.toPath(), filePaths); + } + } + } + } + + /** + * Sanitizes a given path by making sure it is a valid path and does not attempt to traverse outside the given base directory. + * Used so the mod cannot access files outside the base (minecraft) directory. + * + * @param baseDir The base directory to work from. + * @param userProvidedPath The path specified by the user. + * @return The sanitized path. + * @throws SecurityException If the path is invalid or attempts to traverse outside the base directory. + */ + public static Path sanitizePath(Path baseDir, String userProvidedPath) { + try { + // Convert paths to canonical form + Path requestedPath = baseDir.resolve(userProvidedPath).toAbsolutePath().normalize(); + + // Check if the requested path starts with the base directory + if (!requestedPath.startsWith(baseDir)) { + throw new SecurityException("Path traversal attempt detected"); + } + + return requestedPath; + } catch (Exception e) { + throw new SecurityException("Invalid path", e); + } + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/utils/DownloadUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/utils/DownloadUtils.java new file mode 100644 index 0000000..41554bb --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/utils/DownloadUtils.java @@ -0,0 +1,29 @@ +package dev.oxydien.simpleModSync.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.stream.Collectors; + +public class DownloadUtils { + public static String downloadString(String uriString) throws IOException, URISyntaxException { + URL url = new URI(uriString).toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + InputStream inputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String jsonString = reader.lines().collect(Collectors.joining("\n")); + + reader.close(); + inputStream.close(); + connection.disconnect(); + + return jsonString; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/utils/StringUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/utils/StringUtils.java new file mode 100644 index 0000000..c94b4a8 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/utils/StringUtils.java @@ -0,0 +1,11 @@ +package dev.oxydien.simpleModSync.utils; + +public class StringUtils { + public static String sanitize(String input) { + return input.replaceAll("[^a-zA-Z0-9.\\-_]", ""); + } + + public static String sanitizeDirectory(String input) { + return input.replaceAll("[^a-zA-Z0-9. \\-/_\\\\]", ""); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/utils/ZipUtils.java b/common/src/main/java/dev/oxydien/simpleModSync/utils/ZipUtils.java new file mode 100644 index 0000000..f9ea4ff --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/utils/ZipUtils.java @@ -0,0 +1,33 @@ +package dev.oxydien.simpleModSync.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipFile; + +public class ZipUtils { + public static List ExtractZipFile(Path zipFilePath, Path destinationDirectory) throws IOException { + List extractedFiles = new ArrayList<>(); + try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) { + zipFile.entries().asIterator().forEachRemaining(zipEntry -> { + try { + Path path = destinationDirectory.resolve(zipEntry.getName()); + if (zipEntry.isDirectory()) { + Files.createDirectories(path); + extractedFiles.add(path.toAbsolutePath()); + } else { + Files.createDirectories(path.getParent()); + Files.copy(zipFile.getInputStream(zipEntry), path); + extractedFiles.add(path.toAbsolutePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + return extractedFiles; + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/workers/ContentWorker.java b/common/src/main/java/dev/oxydien/simpleModSync/workers/ContentWorker.java new file mode 100644 index 0000000..7596e41 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/workers/ContentWorker.java @@ -0,0 +1,95 @@ +package dev.oxydien.simpleModSync.workers; + +import com.google.gson.JsonObject; +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.content.Content; +import dev.oxydien.simpleModSync.content.SyncSchema; +import dev.oxydien.simpleModSync.content.SyncStatus; +import dev.oxydien.simpleModSync.content.handler.ContentHandler; +import dev.oxydien.simpleModSync.io.FileOperations; +import dev.oxydien.simpleModSync.log.Log; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ContentWorker { + private final FileOperations files; + private final SyncSchema schema; + + public ContentWorker(SyncSchema schema) { + this.schema = schema; + this.files = new FileOperations(schema); + } + + public void Process(JsonObject contentObject, ContentHandler handler, int index) { + long wid = Thread.currentThread().threadId(); + Log.debug("Worker %d is processing index: %d".formatted(wid, index)); + + // Parsing content + this.schema.withStatus(index, (status) -> { + status.setState(SyncStatus.SyncState.PARSING); + }); + + Content content; + try { + content = handler.ParseJson(contentObject, index); + } catch (Exception e) { + this.schema.withStatus(index, (status) -> { + status.setErrorMessage(e.getMessage()); + }); + return; + } + this.schema.setContent(index, content); + + Log.debug("Worker %d parsed: %s: %s".formatted(wid, content.getType().toString(), content.getName())); + + // Check directory + Path contentDir = handler.GetDirectory(SimpleModSync.getInstance().getInstanceDir()); + if (!contentDir.toFile().exists()){ + try { + Files.createDirectories(contentDir); + } catch (IOException e) { + this.schema.withStatus(index, (status) -> { + status.setErrorMessage(e.getMessage()); + }); + return; + } + } + + // Checking versions + boolean needsUpdate = handler.NeedsUpdate(content); + if (!needsUpdate) { + Log.debug("Worker %d finished index %d, without update".formatted(wid, index)); + this.schema.withStatus(index, (status) -> { + status.setState(SyncStatus.SyncState.FINISHED); + }); + return; + } + + try { + Path olderVersion = handler.GetOlderVersion(content); + if (olderVersion != null) { + Log.info("Worker %d found older version of %s".formatted(wid, content.getName())); + + Files.delete(olderVersion); + } + } catch (Exception e) { + this.schema.withStatus(index, (status) -> { + status.setErrorMessage(e.getMessage()); + }); + } + + // Downloading new version + handler.UpdateVersion(content, this.files , index); + + // Finished + if (handler.CheckExistence(content)) { + this.schema.withStatus(index, (status) -> { + status.setState(SyncStatus.SyncState.MODIFIED); + }); + } + + Log.debug("Worker %d finished index %d".formatted(wid, index)); + } +} diff --git a/common/src/main/java/dev/oxydien/simpleModSync/workers/SyncWorker.java b/common/src/main/java/dev/oxydien/simpleModSync/workers/SyncWorker.java new file mode 100644 index 0000000..2b7ea34 --- /dev/null +++ b/common/src/main/java/dev/oxydien/simpleModSync/workers/SyncWorker.java @@ -0,0 +1,275 @@ +package dev.oxydien.simpleModSync.workers; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import dev.oxydien.simpleModSync.HandlerRegistry; +import dev.oxydien.simpleModSync.SimpleModSync; +import dev.oxydien.simpleModSync.config.Config; +import dev.oxydien.simpleModSync.content.SyncSchema; +import dev.oxydien.simpleModSync.content.SyncStatus; +import dev.oxydien.simpleModSync.content.handler.ContentHandler; +import dev.oxydien.simpleModSync.log.Log; +import dev.oxydien.simpleModSync.modification.Modification; +import dev.oxydien.simpleModSync.modification.handler.ModificationHandler; +import dev.oxydien.simpleModSync.utils.DownloadUtils; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public class SyncWorker implements Runnable { + public interface SyncWorkerUpdateCallback { + /// Called whenever the state changes + void update(); + } + + private final SyncSchema schema; + private final ConcurrentHashMap contentObjects = new ConcurrentHashMap<>(); + private final ConcurrentHashMap modificationObjects = new ConcurrentHashMap<>(); + private final CopyOnWriteArraySet contentsToCheck = new CopyOnWriteArraySet<>(); + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final ExecutorService virtualThreadExecutor; + private final AtomicReference syncStatus = new AtomicReference<>(new SyncStatus()); + private final AtomicReference updateCallback = new AtomicReference<>(); + + public SyncWorker(SyncSchema schema) { + this.schema = schema; + this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); + } + + public SyncStatus getStatus() { + return this.syncStatus.get(); + } + + @Override + public void run() { + if (!isRunning.compareAndSet(false, true)) { + return; + } + + this.contentsToCheck.clear(); + + this.changeStatus(SyncStatus.OfState(SyncStatus.SyncState.RETRIEVING_SCHEMA)); + + try { + // Download the base json + String url = Config.instance.getDownloadUrl(); + + if (url.isBlank() || url.length() < 4) { + this.syncStatus.set(SyncStatus.OfState(SyncStatus.SyncState.UNSYNCED)); + Log.warning("Download is empty or invalid, not syncing"); + return; + } + + String jsonString = DownloadUtils.downloadString(url); + + this.syncStatus.set(SyncStatus.OfState(SyncStatus.SyncState.PARSING)); + + JsonObject rootObject = JsonParser.parseString(jsonString).getAsJsonObject(); + SyncSchema.SyncWork work = schema.ParseJson(rootObject); + this.contentsToCheck.addAll(work.contentsToCheck()); + this.extractContentObjects(rootObject); + this.extractModificationObjects(rootObject, work.modificationsToExecute()); + + this.changeStatus(SyncStatus.OfState(SyncStatus.SyncState.DOWNLOADING)); + + this.processAllContent(); + + this.processModifications(work.modificationsToExecute()); + + this.finish(); + } catch (IOException e) { + Log.error("run.SyncWorker.IOException", "Failed to download syncSchema file", e); + this.changeStatus(SyncStatus.OfError("Failed to download syncSchema file")); + } catch (URISyntaxException e) { + Log.error("run.SyncWorker.URISyntaxException", "Invalid syncScheme URL address", e); + this.changeStatus(SyncStatus.OfError("Invalid syncScheme URL address")); + } catch (JsonSyntaxException e) { + Log.error("run.SyncWorker.JsonSyntaxException", "Invalid json format", e); + this.changeStatus(SyncStatus.OfError("Invalid json format")); + } catch (UnsupportedOperationException e) { + Log.error("run.SyncWorker.UnsupportedOperationException", "Unsupported feature", e); + this.changeStatus(SyncStatus.OfError("Unsupported feature")); + } finally { + isRunning.set(false); + } + } + + private void extractContentObjects(JsonObject rootObject) { + this.contentObjects.clear(); + + JsonArray syncArray = new JsonArray(); + if (rootObject.has("sync") && rootObject.get("sync").isJsonArray()) { + syncArray = rootObject.getAsJsonArray("sync"); + } + + for (int i = 0; i < syncArray.size(); i++) { + if (syncArray.get(i).isJsonObject() && this.contentsToCheck.contains(i)) { + this.contentObjects.put(i, syncArray.get(i).getAsJsonObject()); + } + } + } + + private void extractModificationObjects(JsonObject rootObject, List modificationsToExecute) { + this.modificationObjects.clear(); + + JsonArray modifyArray = new JsonArray(); + if (rootObject.has("modify") && rootObject.get("modify").isJsonArray()) { + modifyArray = rootObject.getAsJsonArray("modify"); + } + + for (int i = 0; i < modifyArray.size(); i++) { + if (modifyArray.get(i).isJsonObject() && modificationsToExecute.contains(i)) { + this.modificationObjects.put(i, modifyArray.get(i).getAsJsonObject()); + } else { + Log.debug("Modification at index", i, "has invalid structure"); + } + } + } + + private void processAllContent() { + HandlerRegistry registry = SimpleModSync.getInstance().Handlers; + CountDownLatch latch = new CountDownLatch(contentsToCheck.size()); + + for (Integer index : contentsToCheck) { + this.virtualThreadExecutor.submit(() -> { + try { + this.processContentItem(index, registry); + } catch (Exception e) { + schema.withStatus(index, status -> { + status.setErrorMessage("Unexpected error: " + e.getMessage()); + }); + } finally { + latch.countDown(); + } + }); + } + + // Wait for all workers to complete + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void processContentItem(int index, HandlerRegistry registry) { + JsonObject contentObject = contentObjects.get(index); + if (contentObject == null) { + schema.withStatus(index, status -> { + status.setErrorMessage("Content object not found"); + }); + return; + } + + String type = "mod"; + if (contentObject.has("type") && contentObject.get("type").isJsonPrimitive() + && contentObject.get("type").getAsJsonPrimitive().isString()) { + type = contentObject.get("type").getAsString(); + } + + ContentHandler handler = registry.getContentHandler(type); + if (handler == null) { + schema.withStatus(index, status -> { + status.setState(SyncStatus.SyncState.UNSUPPORTED); + }); + return; + } + + ContentWorker worker = new ContentWorker(schema); + worker.Process(contentObject, handler, index); + } + + private void processModifications(List integers) { + HandlerRegistry registry = SimpleModSync.getInstance().Handlers; + + Log.debug("Running", integers.size(), "modifications"); + + for (Integer index : integers) { + JsonObject modObject = modificationObjects.get(index); + if (modObject == null) { + return; + } + + if (!modObject.has("type") || !modObject.get("type").isJsonPrimitive()) { + Log.warning("Failed to run modification on index", index, ": No valid type specified"); + return; + } + String type = modObject.get("type").getAsString(); + + ModificationHandler handler = registry.getModificationHandler(type); + if (handler == null) { + Log.warning("Failed to run modification on index", index, ": Unsupported type specified", type); + return; + } + + try { + Modification mod = handler.ParseJson(modObject); + handler.Execute(mod, SimpleModSync.getInstance().getInstanceDir()); + } catch (Exception e) { + Log.error("Failed to run modification on index", index, e); + } + } + } + + private void finish() { + var progresses = this.schema.getProgress(); + boolean anyModified = false; + boolean anyError = false; + + for (var iterator = progresses.keys().asIterator(); iterator.hasNext();) { + int key = iterator.next(); + SyncStatus status = progresses.get(key); + + if (status.getState() == SyncStatus.SyncState.ERROR) { + anyError = true; + } + + if (status.getState() == SyncStatus.SyncState.MODIFIED) { + anyModified = true; + } + } + + if (anyError) { + this.changeStatus(SyncStatus.OfState(SyncStatus.SyncState.ERROR)); + } else if (anyModified) { + this.changeStatus(SyncStatus.OfState(SyncStatus.SyncState.MODIFIED)); + } else { + this.changeStatus(SyncStatus.OfState(SyncStatus.SyncState.FINISHED)); + } + } + + private void changeStatus(SyncStatus syncStatus) { + this.syncStatus.set(syncStatus); + if (this.updateCallback.get() != null) { + this.updateCallback.get().update(); + } + } + + public void subscribeUpdateCallback(SyncWorkerUpdateCallback callback) { + this.updateCallback.set(callback); + } + + public boolean isRunning() { + return isRunning.get(); + } + + public void shutdown() { + if (this.virtualThreadExecutor != null && !this.virtualThreadExecutor.isShutdown()) { + this.virtualThreadExecutor.shutdown(); + try { + if (!this.virtualThreadExecutor.awaitTermination(60, TimeUnit.SECONDS)) { + this.virtualThreadExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + this.virtualThreadExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/simplemodsync/lang/cs_cz.json b/common/src/main/resources/assets/simplemodsync/lang/cs_cz.json new file mode 100644 index 0000000..ad0323b --- /dev/null +++ b/common/src/main/resources/assets/simplemodsync/lang/cs_cz.json @@ -0,0 +1,27 @@ +{ + "simple_mod_sync.ui.init_screen.title": "Simple Mod Sync", + "simple_mod_sync.ui.init_screen.sub_header": "Zadejte URL níže a stiskněte tlačítko", + "simple_mod_sync.ui.init_screen.auto_update": "Automatická synchronizace", + "simple_mod_sync.ui.init_screen.save": "Uložit", + "simple_mod_sync.ui.content_screen.title": "§lSimple Mod Sync", + "simple_mod_sync.ui.content_screen.save_url_button": "Uložit URL", + "simple_mod_sync.ui.content_screen.sync_button": "Synchronizovat nyní", + "simple_mod_sync.ui.content_screen.auto_download_true": "Vypnout auto-sync", + "simple_mod_sync.ui.content_screen.auto_download_false": "Povolit auto-sync", + "simple_mod_sync.ui.content_screen.back_button": "Zpět", + "simple_mod_sync.ui.content_type.mod": "Mód", + "simple_mod_sync.ui.content_type.resourcepack": "Balíček zdrojů", + "simple_mod_sync.ui.content_type.shader": "Shader", + "simple_mod_sync.ui.content_type.datapack": "Datový balíček", + "simple_mod_sync.ui.content_type.packed": "Zabaleno", + "simple_mod_sync.ui.sync_state.starting": "Synchronizace právě začala", + "simple_mod_sync.ui.sync_state.parsing": "Parsování souboru šablony", + "simple_mod_sync.ui.sync_state.finished": "Vše hotovo, není vyžadována žádná akce!", + "simple_mod_sync.ui.sync_state.downloading": "Stahování nového obsahu", + "simple_mod_sync.ui.sync_state.modified": "Restartujte hru pro použití aktualizace", + "simple_mod_sync.ui.sync_state.unsynced": "Nesynchronizováno", + "simple_mod_sync.ui.sync_state.retrieving_schema": "Načítání souboru šablony", + "simple_mod_sync.ui.sync_state.invalid": "Neplatná struktura, více v logu", + "simple_mod_sync.ui.sync_state.unsupported": "Nepodporovaná funkce, více v logu", + "simple_mod_sync.ui.sync_state.error": "Něco se pokazilo, více v herním logu" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/simplemodsync/lang/en_us.json b/common/src/main/resources/assets/simplemodsync/lang/en_us.json new file mode 100644 index 0000000..26369a0 --- /dev/null +++ b/common/src/main/resources/assets/simplemodsync/lang/en_us.json @@ -0,0 +1,27 @@ +{ + "simple_mod_sync.ui.init_screen.title": "Simple Mod Sync", + "simple_mod_sync.ui.init_screen.sub_header": "Enter the URL bellow and press the button", + "simple_mod_sync.ui.init_screen.auto_update": "Auto synchronize", + "simple_mod_sync.ui.init_screen.save": "Save", + "simple_mod_sync.ui.content_screen.title": "§lSimple Mod Sync", + "simple_mod_sync.ui.content_screen.save_url_button": "Save URL", + "simple_mod_sync.ui.content_screen.sync_button": "Sync now", + "simple_mod_sync.ui.content_screen.auto_download_true": "Disable auto", + "simple_mod_sync.ui.content_screen.auto_download_false": "Enable auto", + "simple_mod_sync.ui.content_screen.back_button": "Back", + "simple_mod_sync.ui.content_type.mod": "Mod", + "simple_mod_sync.ui.content_type.resourcepack": "Resource pack", + "simple_mod_sync.ui.content_type.shader": "Shader", + "simple_mod_sync.ui.content_type.datapack": "Datapack", + "simple_mod_sync.ui.content_type.packed": "Packed", + "simple_mod_sync.ui.sync_state.starting": "The synchronization just started", + "simple_mod_sync.ui.sync_state.parsing": "Parsing the schema file", + "simple_mod_sync.ui.sync_state.finished": "All done, no action required!", + "simple_mod_sync.ui.sync_state.downloading": "Downloading new content", + "simple_mod_sync.ui.sync_state.modified": "Restart your game to apply update", + "simple_mod_sync.ui.sync_state.unsynced": "Not synchronized", + "simple_mod_sync.ui.sync_state.retrieving_schema": "Retrieving the schema file", + "simple_mod_sync.ui.sync_state.invalid": "Invalid structure, see more in logs", + "simple_mod_sync.ui.sync_state.unsupported": "Unsupported feature, see more in logs", + "simple_mod_sync.ui.sync_state.error": "Something went wrong, see more in game logs" +} diff --git a/common/src/main/resources/assets/simplemodsync/lang/nl_nl.json b/common/src/main/resources/assets/simplemodsync/lang/nl_nl.json new file mode 100644 index 0000000..547a4a9 --- /dev/null +++ b/common/src/main/resources/assets/simplemodsync/lang/nl_nl.json @@ -0,0 +1,27 @@ +{ + "simple_mod_sync.ui.init_screen.title": "Simple Mod Sync", + "simple_mod_sync.ui.init_screen.sub_header": "Voer de URL hieronder in en druk op de knop", + "simple_mod_sync.ui.init_screen.auto_update": "Automatisch synchroniseren", + "simple_mod_sync.ui.init_screen.save": "Opslaan", + "simple_mod_sync.ui.content_screen.title": "§lSimple Mod Sync", + "simple_mod_sync.ui.content_screen.save_url_button": "URL opslaan", + "simple_mod_sync.ui.content_screen.sync_button": "Nu synchroniseren", + "simple_mod_sync.ui.content_screen.auto_download_true": "Auto uitschakelen", + "simple_mod_sync.ui.content_screen.auto_download_false": "Auto inschakelen", + "simple_mod_sync.ui.content_screen.back_button": "Terug", + "simple_mod_sync.ui.content_type.mod": "Mod", + "simple_mod_sync.ui.content_type.resourcepack": "Resource pack", + "simple_mod_sync.ui.content_type.shader": "Shader", + "simple_mod_sync.ui.content_type.datapack": "Datapack", + "simple_mod_sync.ui.content_type.packed": "Ingepakt", + "simple_mod_sync.ui.sync_state.starting": "De synchronisatie is zojuist gestart", + "simple_mod_sync.ui.sync_state.parsing": "Schemabestand parseren", + "simple_mod_sync.ui.sync_state.finished": "Alles voltooid, geen actie vereist!", + "simple_mod_sync.ui.sync_state.downloading": "Nieuwe inhoud downloaden", + "simple_mod_sync.ui.sync_state.modified": "Herstart je spel om de update toe te passen", + "simple_mod_sync.ui.sync_state.unsynced": "Niet gesynchroniseerd", + "simple_mod_sync.ui.sync_state.retrieving_schema": "Schemabestand ophalen", + "simple_mod_sync.ui.sync_state.invalid": "Ongeldige structuur, zie logs voor meer info", + "simple_mod_sync.ui.sync_state.unsupported": "Niet-ondersteunde functie, zie logs voor meer info", + "simple_mod_sync.ui.sync_state.error": "Er is iets misgegaan, zie de game-logs voor meer info" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.aseprite new file mode 100644 index 0000000..1d06c40 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.png new file mode 100644 index 0000000..b07b595 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/datapack.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.aseprite new file mode 100644 index 0000000..dfbf0d9 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.png new file mode 100644 index 0000000..f24273f Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/downloading.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.aseprite new file mode 100644 index 0000000..6715151 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.png new file mode 100644 index 0000000..6983ae4 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/error.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.aseprite new file mode 100644 index 0000000..55c0d6f Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.png new file mode 100644 index 0000000..78ce3e7 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/finished.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.aseprite new file mode 100644 index 0000000..7fdd52d Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.png new file mode 100644 index 0000000..28146d8 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/loading.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.aseprite new file mode 100644 index 0000000..8066a91 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.png new file mode 100644 index 0000000..ecdb086 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/mod.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.aseprite new file mode 100644 index 0000000..56d8c8f Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.png new file mode 100644 index 0000000..7442d8a Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/modified.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.aseprite new file mode 100644 index 0000000..90bd9f1 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.png new file mode 100644 index 0000000..9a407a2 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/packed.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.aseprite new file mode 100644 index 0000000..8982da8 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.png new file mode 100644 index 0000000..020c7ee Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/parsing.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.aseprite new file mode 100644 index 0000000..1cc354d Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.png new file mode 100644 index 0000000..258a80e Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/preparing.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.aseprite new file mode 100644 index 0000000..cb427bd Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.png new file mode 100644 index 0000000..567c344 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/resourcepack.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.aseprite new file mode 100644 index 0000000..30de714 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.png new file mode 100644 index 0000000..5b11781 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/shader.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.aseprite new file mode 100644 index 0000000..3199de4 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.png new file mode 100644 index 0000000..22f2f22 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unknown.png differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.aseprite b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.aseprite new file mode 100644 index 0000000..e8eca4a Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.aseprite differ diff --git a/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.png b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.png new file mode 100644 index 0000000..dc18556 Binary files /dev/null and b/common/src/main/resources/assets/simplemodsync/ui/textures/icons/unsynced.png differ diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 0000000..f82b365 --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'multiloader-loader' + id 'fabric-loom' +} +dependencies { + minecraft "com.mojang:minecraft:${minecraft_version}" + mappings loom.layered { + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") + } + modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" +} + +loom { + def aw = project(':common').file("src/main/resources/${mod_id}.accesswidener") + if (aw.exists()) { + accessWidenerPath.set(aw) + } + mixin { + defaultRefmapName.set("${mod_id}.refmap.json") + } + runs { + client { + client() + setConfigName('Fabric Client') + ideConfigGenerated(true) + runDir('runs/client') + } + server { + server() + setConfigName('Fabric Server') + ideConfigGenerated(true) + runDir('runs/server') + } + } +} \ No newline at end of file diff --git a/fabric/src/main/java/dev/oxydien/simpleModSync/fabric/SimpleModSyncFabric.java b/fabric/src/main/java/dev/oxydien/simpleModSync/fabric/SimpleModSyncFabric.java new file mode 100644 index 0000000..79a1d0e --- /dev/null +++ b/fabric/src/main/java/dev/oxydien/simpleModSync/fabric/SimpleModSyncFabric.java @@ -0,0 +1,20 @@ +package dev.oxydien.simpleModSync.fabric; + +import dev.oxydien.simpleModSync.SimpleModSync; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; + +import java.nio.file.Path; + +public class SimpleModSyncFabric extends SimpleModSync implements ModInitializer { + @Override + public void onInitialize() { + SimpleModSync.init(this); + super.onInitialize(); + } + + @Override + public Path getInstanceDir() { + return FabricLoader.getInstance().getGameDir(); + } +} diff --git a/src/main/resources/assets/simple-mod-sync/icon.png b/fabric/src/main/resources/assets/simplemodsync/icon.png similarity index 100% rename from src/main/resources/assets/simple-mod-sync/icon.png rename to fabric/src/main/resources/assets/simplemodsync/icon.png diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..b574efc --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "simple-mod-sync", + "version": "${version}", + "name": "Simple Mod Sync", + "description": "A lightweight mod for synchronizing game mods via a URL-based schema.", + "authors": [ + "oxydien" + ], + "contact": { + "homepage": "https://modrinth.com/mod/simple-mod-sync", + "sources": "https://github.com/oxydien/simple-mod-sync" + }, + "license": "MIT", + "icon": "assets/simplemodsync/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "dev.oxydien.simpleModSync.fabric.SimpleModSyncFabric" + ] + }, + "mixins": [ + "simplemodsync.mixins.json" + ], + "depends": { + "fabricloader": ">=0.16.0", + "minecraft": ">=1.21.1", + "java": ">=17", + "fabric-api": "*" + } +} diff --git a/src/main/resources/simple-mod-sync.mixins.json b/fabric/src/main/resources/simplemodsync.mixins.json similarity index 74% rename from src/main/resources/simple-mod-sync.mixins.json rename to fabric/src/main/resources/simplemodsync.mixins.json index 1c88c03..e6ed992 100644 --- a/src/main/resources/simple-mod-sync.mixins.json +++ b/fabric/src/main/resources/simplemodsync.mixins.json @@ -1,6 +1,6 @@ { "required": true, - "package": "dev.oxydien.mixin", + "package": "dev.oxydien.simpleModSync.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ "TitleScreenMixin" diff --git a/gradle.properties b/gradle.properties index 4556793..d56d32e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,28 @@ -# Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G -org.gradle.parallel=true +version=1.3.0 +group=dev.oxydien.simpleModSync +java_version=21 -# Fabric Properties -# check these on https://fabricmc.net/develop +# Common minecraft_version=1.21.5 -yarn_mappings=1.21.5+build.1 -loader_version=0.16.14 +mod_name=SimpleModSync +mod_author=oxydien +mod_id=simplemodsync +license=MIT +credits=oxydien +description=A simple way to share and sync mods, resource packs, shaders, and other Minecraft content with your friends or community. +minecraft_version_range=[1.21.5, 1.22) +neo_form_version=1.21.5-20250325.162830 +parchment_minecraft=1.21.4 +parchment_version=2025.03.23 -# Mod Properties -mod_version=1.2.0 -maven_group=dev.oxydien -archives_base_name=simple-mod-sync +# Fabric +fabric_version=0.119.5+1.21.5 +fabric_loader_version=0.16.10 -# Dependencies -fabric_version=0.125.3+1.21.5 +# NeoForge +neoforge_version=21.5.4-beta +neoforge_loader_version_range=[4,) + +# Gradle +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..adda1f2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 0000000..acb78aa --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'multiloader-loader' + id 'net.neoforged.moddev' +} + +neoForge { + version = neoforge_version + // Automatically enable neoforge AccessTransformers if the file exists + def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.from(at.absolutePath) + } + parchment { + minecraftVersion = parchment_minecraft + mappingsVersion = parchment_version + } + runs { + configureEach { + systemProperty('neoforge.enabledGameTestNamespaces', mod_id) + ideName = "NeoForge ${it.name.capitalize()} (${project.path})" // Unify the run config names with fabric + } + client { + client() + } + data { + clientData() + } + server { + server() + } + } + mods { + "${mod_id}" { + sourceSet sourceSets.main + } + } +} + +sourceSets.main.resources { srcDir 'src/generated/resources' } \ No newline at end of file diff --git a/neoforge/src/main/java/dev/oxydien/simpleModSync/neoforge/SimpleModSyncNeoforge.java b/neoforge/src/main/java/dev/oxydien/simpleModSync/neoforge/SimpleModSyncNeoforge.java new file mode 100644 index 0000000..76458fe --- /dev/null +++ b/neoforge/src/main/java/dev/oxydien/simpleModSync/neoforge/SimpleModSyncNeoforge.java @@ -0,0 +1,21 @@ +package dev.oxydien.simpleModSync.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLPaths; +import dev.oxydien.simpleModSync.SimpleModSync; +import java.nio.file.Path; + +@Mod(SimpleModSync.MOD_ID) +public class SimpleModSyncNeoforge extends SimpleModSync { + + public SimpleModSyncNeoforge(IEventBus eventBus) { + SimpleModSync.init(this); + super.onInitialize(); + } + + @Override + public Path getInstanceDir() { + return FMLPaths.GAMEDIR.get(); + } +} \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..1fdc200 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,32 @@ +modLoader = "javafml" #mandatory +loaderVersion = "${neoforge_loader_version_range}" #mandatory +license = "${license}" # Review your options at https://choosealicense.com/. +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional + +[[mods]] #mandatory +modId = "${mod_id}" #mandatory +version = "${version}" #mandatory +displayName = "${mod_name}" #mandatory +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional (see https://docs.neoforged.net/docs/misc/updatechecker/) +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional (displayed in the mod UI) +logoFile="${mod_id}.png" #optional +credits="${credits}" #optional +authors = "${mod_author}" #optional +description = '''${description}''' #mandatory (Supports multiline text) + +[[mixins]] +config = "${mod_id}.mixins.json" + +[[dependencies.${mod_id}]] #optional +modId = "neoforge" #mandatory +type="required" #mandatory (Can be one of "required", "optional", "incompatible" or "discouraged") +versionRange = "[${neoforge_version},)" #mandatory +ordering = "NONE" # The order that this dependency should load in relation to your mod, required to be either 'BEFORE' or 'AFTER' if the dependency is not mandatory +side = "BOTH" # Side this dependency is applied on - 'BOTH', 'CLIENT' or 'SERVER' + +[[dependencies.${mod_id}]] +modId = "minecraft" +type="required" #mandatory (Can be one of "required", "optional", "incompatible" or "discouraged") +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" diff --git a/neoforge/src/main/resources/simplemodsync.mixins.json b/neoforge/src/main/resources/simplemodsync.mixins.json new file mode 100644 index 0000000..ede68aa --- /dev/null +++ b/neoforge/src/main/resources/simplemodsync.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.oxydien.simpleModSync.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [ + "TitleScreenMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/neoforge/src/main/resources/simplemodsync.png b/neoforge/src/main/resources/simplemodsync.png new file mode 100644 index 0000000..9612763 Binary files /dev/null and b/neoforge/src/main/resources/simplemodsync.png differ diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..09b9dac --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,49 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "21" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Qodana supports other languages, for example, Python, JavaScript, TypeScript, Go, C#, PHP +#For all supported languages see https://www.jetbrains.com/help/qodana/linters.html +linter: jetbrains/qodana-jvm-community:2025.2 diff --git a/settings.gradle b/settings.gradle index 75c4d72..5fb683d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,38 @@ pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - mavenCentral() - gradlePluginPortal() - } -} \ No newline at end of file + repositories { + gradlePluginPortal() + mavenCentral() + exclusiveContent { + forRepository { + maven { + name = 'Fabric' + url = uri('https://maven.fabricmc.net') + } + } + filter { + includeGroup('net.fabricmc') + includeGroup('fabric-loom') + } + } + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = uri('https://repo.spongepowered.org/repository/maven-public') + } + } + filter { + includeGroupAndSubgroups("org.spongepowered") + } + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +rootProject.name = 'simple-mod-sync' +include('common') +include('fabric') +include('neoforge') diff --git a/src/main/java/dev/oxydien/SimpleModSync.java b/src/main/java/dev/oxydien/SimpleModSync.java deleted file mode 100644 index 07bf654..0000000 --- a/src/main/java/dev/oxydien/SimpleModSync.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.oxydien; - -import dev.oxydien.config.Config; -import dev.oxydien.logger.Log; -import dev.oxydien.workers.ModDownloadWorker; -import net.fabricmc.api.ModInitializer; - -import net.fabricmc.loader.api.FabricLoader; - -public class SimpleModSync implements ModInitializer { - public static final String MOD_ID = "simple-mod-sync"; - - public static ModDownloadWorker worker; - - @Override - public void onInitialize() { - new Log(MOD_ID, true); - Log.Log.info("Simple Mod Sync is starting up..."); - FabricLoader loader = FabricLoader.getInstance(); - String configPath = loader.getConfigDir() + "/" + MOD_ID + ".json"; - String destPath = loader.getGameDir().toString(); - new Config(configPath, destPath); - - worker = new ModDownloadWorker(); - if (Config.instance.getAutoDownload()) { - SimpleModSync.StartWorker(); - } - } - - public static void StartWorker() { - if (worker.GetProgress() != 0 && worker.GetProgress() != 100) { - Log.Log.debug("start-worker", "Worker already started {}", worker.GetProgress()); - return; - } - worker.start(); - } -} diff --git a/src/main/java/dev/oxydien/SimpleModSyncDataGenerator.java b/src/main/java/dev/oxydien/SimpleModSyncDataGenerator.java deleted file mode 100644 index ab5eef7..0000000 --- a/src/main/java/dev/oxydien/SimpleModSyncDataGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.oxydien; - -import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; -import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; - -public class SimpleModSyncDataGenerator implements DataGeneratorEntrypoint { - @Override - public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { - // unused - } -} diff --git a/src/main/java/dev/oxydien/data/ContentSyncProgress.java b/src/main/java/dev/oxydien/data/ContentSyncProgress.java deleted file mode 100644 index 8cc6c3a..0000000 --- a/src/main/java/dev/oxydien/data/ContentSyncProgress.java +++ /dev/null @@ -1,53 +0,0 @@ -package dev.oxydien.data; - -import dev.oxydien.enums.ContentSyncOutcome; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -public class ContentSyncProgress { - - private final int index; - private final AtomicInteger progress; - private AtomicReference outcome; - private AtomicReference exception; - - public ContentSyncProgress(int index, int progress) { - this.index = index; - this.progress = new AtomicInteger(progress); - this.outcome = new AtomicReference<>(ContentSyncOutcome.IN_PROGRESS); - this.exception = new AtomicReference<>(null); - } - - public int getIndex() { - return this.index; - } - - public int getProgress() { - return this.progress.get(); - } - - public ContentSyncOutcome getOutcome() { - return this.outcome.get(); - } - - public Exception getException() { - return this.exception.get(); - } - - public void setProgress(int progress) { - this.progress.set(progress); - } - - public void setOutcome(ContentSyncOutcome outcome, @Nullable Exception exception) { - this.outcome.set(outcome); - this.exception.set(exception); - } - - public boolean isError() { - var outcome = this.getOutcome(); - return outcome == ContentSyncOutcome.INVALID_URL || outcome == ContentSyncOutcome.DOWNLOAD_INTERRUPTED || - outcome == ContentSyncOutcome.ALREADY_EXISTS; - } -} diff --git a/src/main/java/dev/oxydien/data/ProgressCallback.java b/src/main/java/dev/oxydien/data/ProgressCallback.java deleted file mode 100644 index 2ee8512..0000000 --- a/src/main/java/dev/oxydien/data/ProgressCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.oxydien.data; - -import dev.oxydien.enums.CallbackReason; - -public interface ProgressCallback { - void onProgress(CallbackReason reason); -} diff --git a/src/main/java/dev/oxydien/data/SyncData.java b/src/main/java/dev/oxydien/data/SyncData.java deleted file mode 100644 index fdfda3b..0000000 --- a/src/main/java/dev/oxydien/data/SyncData.java +++ /dev/null @@ -1,216 +0,0 @@ -package dev.oxydien.data; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import dev.oxydien.enums.SyncModificationType; -import net.minecraft.util.JsonHelper; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class SyncData { - public static final List ALLOWED_SYNC_VERSIONS = List.of(1,2,3); - - private final int syncVersion; - private final List sync; - private final List modify; - - public SyncData(int syncVersion, List content, List modify) { - this.syncVersion = syncVersion; - this.sync = content; - this.modify = modify; - } - - /// Used for parsing the sync data - public static SyncData fromJson(JsonObject jsonObject) { - // Check if the sync version is supported - int syncVersion = JsonHelper.getInt(jsonObject, "sync_version"); - if (!ALLOWED_SYNC_VERSIONS.contains(syncVersion)) { - throw new IllegalArgumentException("Invalid sync version: " + syncVersion); - } - - List contentList = new ArrayList<>(); - List modifications = new ArrayList<>(); - - // Parse the sync data content - int index = 0; - JsonElement contentElement = jsonObject.get("sync"); - if (contentElement == null) { - contentElement = jsonObject.get("content"); - } - if (!contentElement.isJsonArray()) { - throw new IllegalArgumentException("'sync' or 'content' is not an array in sync data"); - } - for (JsonElement element : contentElement.getAsJsonArray()) { - JsonObject contentObject = element.getAsJsonObject(); - - SyncData.Content content = SyncData.Content.fromJson(index, contentObject); - contentList.add(content); - index++; - } - - // Parse the sync data modifications (if any) - JsonElement modifyElement = jsonObject.get("modify"); - if (modifyElement != null && modifyElement.isJsonArray()) { - index = 0; - for (JsonElement element : modifyElement.getAsJsonArray()) { - JsonObject modifyObject = element.getAsJsonObject(); - SyncData.Modification modification = SyncData.Modification.fromJson(index, modifyObject); - modifications.add(modification); - index++; - } - } - - return new SyncData(syncVersion, contentList, modifications); - } - - public int getSyncVersion() { - return syncVersion; - } - - public List getContent() { - return Collections.unmodifiableList(this.sync); - } - - public List getModify() { - return Collections.unmodifiableList(this.modify); - } - - public static class Content { - /// Used for assigning status - private final int index; - /// Used for downloading the given content - private final String url; - /// Used to determine if the content is already downloaded, or should be updated / removed - private final String version; - /// File name (prefix, without file extension) - private final String name; - /// Used to determine the folder where the content should be downloaded - @Nullable private final String directoryOverride; - /// File type (mod, resourcepack, shaderpack, datapack, config) - @Nullable private final String type; - - public Content(int index, String url, String version, String fileName, @Nullable String directory, @Nullable String type) { - this.index = index; - this.url = url; - this.version = version; - this.name = fileName; - this.directoryOverride = directory; - this.type = type; - } - - /// Used for parsing the sync data - public static Content fromJson(int index, JsonObject jsonObject) { - String url = JsonHelper.getString(jsonObject, "url"); - String version = JsonHelper.getString(jsonObject, "version"); - String name = jsonObject.has("name") ? JsonHelper.getString(jsonObject, "name") : JsonHelper.getString(jsonObject, "mod_name"); - String directory = jsonObject.get("directory") != null ? jsonObject.get("directory").getAsString() : null; - String type = jsonObject.get("type") != null ? jsonObject.get("type").getAsString() : null; - return new SyncData.Content(index, url, version, name, directory, type); - } - - /// Used for assigning status - public int getIndex() { - return this.index; - } - /// Used for downloading the given content - public String getUrl() { - return this.url; - } - /// File name (prefix, without file extension) - public String getName() { - return this.name; - } - /// Used to determine if the content is already downloaded, or should be updated / removed - public String getVersion() { - return this.version; - } - /// Used to determine the folder where the content should be downloaded - @Nullable - public String getDirectoryOverride() { - return this.directoryOverride; - } - /// File type (mod, resourcepack, shaderpack, datapack, config) - @Nullable - public String getType() { - return this.type; - } - - - /** - * Gets the folder name for the given content type. - *

This is used to determine the folder where the content should be downloaded. - * @return The folder name (e.g. "mods", "resourcepacks", etc.) - */ - public String getTypeFolder() { - return switch (this.getType()) { - case "resourcepack" -> "resourcepacks"; - case "shader" -> "shaderpacks"; - case "datapack" -> "datapacks"; - case "config" -> "config"; - case null, default -> "mods"; - }; - } - - /** - * Gets the file extension for the given content type. - *

This is used to determine the file extension when downloading the content. - * @return The file extension (e.g. ".jar", ".zip") - */ - public String getFileExtension() { - return switch (this.getType()) { - case "resourcepack", "shader", "datapack" -> ".zip"; - case null, default -> ".jar"; - }; - } - } - - public static class Modification { - private final int index; - private final SyncModificationType type; - private final String pattern; - private final String path; - @Nullable private final String result; - - public Modification(int index, SyncModificationType type, String path, String pattern, @Nullable String result) { - this.index = index; - this.type = type; - this.pattern = pattern; - this.path = path; - this.result = result; - } - - public static Modification fromJson(int index, JsonObject jsonObject) { - String typeStr = JsonHelper.getString(jsonObject, "type"); - SyncModificationType type; - switch (typeStr) { - case "remove" -> type = SyncModificationType.REMOVE; - case "rename" -> type = SyncModificationType.RENAME; - default -> type = null; - } - String path = JsonHelper.getString(jsonObject, "path"); - String pattern = JsonHelper.getString(jsonObject, "pattern"); - String result = jsonObject.get("result") != null ? jsonObject.get("result").getAsString() : null; - return new SyncData.Modification(index, type, path, pattern, result); - } - - public int getIndex() { - return this.index; - } - public SyncModificationType getType() { - return this.type; - } - public String getPattern() { - return this.pattern; - } - public String getPath() { - return this.path; - } - @Nullable - public String getResult() { - return this.result; - } - } -} diff --git a/src/main/java/dev/oxydien/enums/CallbackReason.java b/src/main/java/dev/oxydien/enums/CallbackReason.java deleted file mode 100644 index ee8b52c..0000000 --- a/src/main/java/dev/oxydien/enums/CallbackReason.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.oxydien.enums; - -public enum CallbackReason { - NONE, - UPDATE, - UNSUBSCRIBED, -} diff --git a/src/main/java/dev/oxydien/enums/ContentSyncOutcome.java b/src/main/java/dev/oxydien/enums/ContentSyncOutcome.java deleted file mode 100644 index 8f5a179..0000000 --- a/src/main/java/dev/oxydien/enums/ContentSyncOutcome.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.oxydien.enums; - -public enum ContentSyncOutcome { - IN_PROGRESS, - SUCCESS, - ALREADY_EXISTS, - INVALID_URL, - DOWNLOAD_INTERRUPTED, - SUSPICIOUS_CONTENT, -} diff --git a/src/main/java/dev/oxydien/enums/SyncErrorType.java b/src/main/java/dev/oxydien/enums/SyncErrorType.java deleted file mode 100644 index 42972ba..0000000 --- a/src/main/java/dev/oxydien/enums/SyncErrorType.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.oxydien.enums; - -public enum SyncErrorType { - NONE, - REMOTE_NOT_SET, - REMOTE_NOT_FOUND, - PARSING_FAILED, - DOWNLOAD_FAILED, - FAILED_TO_WRITE_FILE, -} diff --git a/src/main/java/dev/oxydien/enums/SyncModificationType.java b/src/main/java/dev/oxydien/enums/SyncModificationType.java deleted file mode 100644 index 3ed5352..0000000 --- a/src/main/java/dev/oxydien/enums/SyncModificationType.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.oxydien.enums; - -public enum SyncModificationType { - RENAME, - REMOVE, -} diff --git a/src/main/java/dev/oxydien/enums/SyncState.java b/src/main/java/dev/oxydien/enums/SyncState.java deleted file mode 100644 index 291ea4d..0000000 --- a/src/main/java/dev/oxydien/enums/SyncState.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.oxydien.enums; - -public enum SyncState { - INITIALIZING, - CHECKING_REMOTE, - PARSING_REMOTE, - DOWNLOADING, - MODIFICATIONS, - READY, - DID_NOT_SYNC, - NEEDS_RESTART, - ERROR -} diff --git a/src/main/java/dev/oxydien/logger/Log.java b/src/main/java/dev/oxydien/logger/Log.java deleted file mode 100644 index 54162c7..0000000 --- a/src/main/java/dev/oxydien/logger/Log.java +++ /dev/null @@ -1,125 +0,0 @@ -package dev.oxydien.logger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Log { - public static Log Log = null; - - private final String name; - private final Logger logger; - private final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{}"); - - public Log(String logName, boolean setStatic) { - this.name = logName; - this.logger = LoggerFactory.getLogger(this.name); - - if (setStatic) { - Log = this; - } - } - - public enum LogLevel { - INFO, WARN, DEBUG, DEV, ERROR, FATAL - } - - public void log(LogLevel level, String placement, String message, Object... args) { - String formattedMessage = this.formatMessage(message, args); - String logMessage = String.format("[%s] (%s): %s", this.name, placement, formattedMessage); - - switch (level) { - case INFO: - logger.info(logMessage); - break; - case WARN: - logger.warn(logMessage); - break; - case DEBUG: - logger.info("[DEBUG] {}", logMessage); - break; - case DEV: - logger.info("[DEV] {}", logMessage); - break; - case ERROR: - logger.error(logMessage); - break; - case FATAL: - logger.error("[FATAL] {}", logMessage); - break; - } - } - - private String formatMessage(String message, Object... args) { - if (args == null || args.length == 0) { - return message; - } - - Matcher matcher = PLACEHOLDER_PATTERN.matcher(message); - StringBuilder result = new StringBuilder(); - int argIndex = 0; - - while (matcher.find()) { - if (argIndex < args.length) { - matcher.appendReplacement(result, Matcher.quoteReplacement(String.valueOf(args[argIndex]))); - argIndex++; - } else { - matcher.appendReplacement(result, Matcher.quoteReplacement("{}")); - } - } - matcher.appendTail(result); - - while (argIndex < args.length) { - result.append(" ").append(args[argIndex]); - argIndex++; - } - - return result.toString(); - } - - public void info(String message, Object... args) { - this.log(LogLevel.INFO, "", message, args); - } - - public void info(String placement, String message, Object... args) { - this.log(LogLevel.INFO, placement, message, args); - } - - public void warn(String message, Object... args) { - this.log(LogLevel.WARN, "", message, args); - } - - public void warn(String placement, String message, Object... args) { - this.log(LogLevel.WARN, placement, message, args); - } - - public void debug(String message, Object... args) { - this.log(LogLevel.DEBUG, "", message, args); - } - - public void debug(String placement, String message, Object... args) { - this.log(LogLevel.DEBUG, placement, message, args); - } - - // Dev does not have a method without placement, so the developer can find the logs - public void dev(String placement, String message, Object... args) { - this.log(LogLevel.DEV, placement, message, args); - } - - public void error(String message, Object... args) { - this.log(LogLevel.ERROR, "", message, args); - } - - public void error(String placement, String message, Object... args) { - this.log(LogLevel.ERROR, placement, message, args); - } - - public void fatal(String message, Object... args) { - this.log(LogLevel.FATAL, "", message, args); - } - - public void fatal(String placement, String message, Object... args) { - this.log(LogLevel.FATAL, placement, message, args); - } -} \ No newline at end of file diff --git a/src/main/java/dev/oxydien/mixin/TitleScreenMixin.java b/src/main/java/dev/oxydien/mixin/TitleScreenMixin.java deleted file mode 100644 index 5fc4864..0000000 --- a/src/main/java/dev/oxydien/mixin/TitleScreenMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.oxydien.mixin; - -import dev.oxydien.SimpleModSync; -import dev.oxydien.config.Config; -import dev.oxydien.enums.SyncErrorType; -import dev.oxydien.enums.SyncState; -import dev.oxydien.ui.SetSyncRemoteScreen; -import dev.oxydien.ui.widget.SyncOverviewWidget; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.text.Text; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(TitleScreen.class) -public class TitleScreenMixin extends Screen { - protected TitleScreenMixin(Text title) { - super(title); - } - - @Inject(at = @At("RETURN"), method = "init") - private void simple_mod_sync$init(CallbackInfo ci) { - SyncOverviewWidget simple_mod_sync$overview = new SyncOverviewWidget(this.textRenderer); - this.addDrawableChild(simple_mod_sync$overview); - - if (SimpleModSync.worker.GetState() == SyncState.ERROR) { - if (SimpleModSync.worker.GetErrorType() == SyncErrorType.REMOTE_NOT_SET) { - if (Config.instance.getDownloadUrl().isEmpty()) { - MinecraftClient.getInstance().setScreen( - new SetSyncRemoteScreen(Text.empty(), this) - ); - } - } - } - } -} diff --git a/src/main/java/dev/oxydien/networking/FileDownloader.java b/src/main/java/dev/oxydien/networking/FileDownloader.java deleted file mode 100644 index 618ea7c..0000000 --- a/src/main/java/dev/oxydien/networking/FileDownloader.java +++ /dev/null @@ -1,91 +0,0 @@ -package dev.oxydien.networking; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Collectors; - -public class FileDownloader { - public static String downloadString(String uriString) throws IOException, URISyntaxException { - URL url = new URI(uriString).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - InputStream inputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String jsonString = reader.lines().collect(Collectors.joining("\n")); - - reader.close(); - inputStream.close(); - connection.disconnect(); - - return jsonString; - } - - public static String downloadFile(String uriString, String outputFileName) throws IOException, URISyntaxException { - URL url = new URI(uriString).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - InputStream inputStream = connection.getInputStream(); - - Path outputPath = Paths.get(outputFileName); - Files.copy(inputStream, outputPath); - - inputStream.close(); - connection.disconnect(); - - return outputPath.toString(); - } - - public interface ProgressCallback { - void onProgress(int percentage); - } - - public static void downloadFileWithProgress(String uriString, String outputFileName, ProgressCallback callback) throws IOException, URISyntaxException { - URL url = new URI(uriString).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - long fileSize = connection.getContentLengthLong(); - InputStream inputStream = connection.getInputStream(); - - Path outputPath = Paths.get(outputFileName); - OutputStream outputStream = Files.newOutputStream(outputPath); - - byte[] buffer = new byte[4096]; - int bytesRead; - long totalBytesRead = 0; - int lastReportedProgress = -1; - - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - totalBytesRead += bytesRead; - - if (fileSize > 0) { // Only calculate progress if file size is known - int progress = (int) ((totalBytesRead * 100) / fileSize); - if (progress > lastReportedProgress) { - callback.onProgress(progress); - lastReportedProgress = progress; - } - } - } - - outputStream.close(); - inputStream.close(); - connection.disconnect(); - - if (lastReportedProgress < 100) { - callback.onProgress(100); - } - } - - public static boolean fileExists(String path) { - Path filePath = Paths.get(path); - return Files.exists(filePath); - } -} diff --git a/src/main/java/dev/oxydien/ui/SetSyncRemoteScreen.java b/src/main/java/dev/oxydien/ui/SetSyncRemoteScreen.java deleted file mode 100644 index a74b5da..0000000 --- a/src/main/java/dev/oxydien/ui/SetSyncRemoteScreen.java +++ /dev/null @@ -1,100 +0,0 @@ -package dev.oxydien.ui; - -import dev.oxydien.SimpleModSync; -import dev.oxydien.config.Config; -import dev.oxydien.ui.widget.SimpleBackgroundWidget; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.*; -import net.minecraft.text.Text; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class SetSyncRemoteScreen extends Screen { - private final Screen parent; - - public SetSyncRemoteScreen(Text title, Screen parent) { - super(title); - this.parent = parent; - } - - @Override - public void init() { - super.init(); - // Background widget - this.addDrawableChild(new SimpleBackgroundWidget(0, 0, this.width, this.height, 0xF1000000)); - - // Subtitle widget - Text subtitleText = Text.translatable("simple_mod_sync.ui.set_sync_screen.subtitle"); - int x = this.width / 2 - subtitleText.getString().length() - 30; - int y = this.getPosY(2) + 5; - MultilineTextWidget subtitleWidget = new MultilineTextWidget(x, y, subtitleText, this.textRenderer); - subtitleWidget.setMaxWidth(this.width - 80); - subtitleWidget.setMaxRows(2); - this.addDrawableChild(subtitleWidget); - - // URL field widget - int urlFieldX = this.width / 2 - 100; - int urlFieldY = this.getPosY(3); - int urlFieldWidth = 200; - int urlFieldHeight = 20; - TextFieldWidget remoteUrl = new TextFieldWidget(this.textRenderer, urlFieldX, urlFieldY, urlFieldWidth, urlFieldHeight, Text.literal("")); - remoteUrl.setMaxLength(368); - this.addDrawableChild(remoteUrl); - - // Auto download toggle button widget - Text autoDownloadTextTrue = Text.translatable("simple_mod_sync.ui.set_sync_screen.auto_download_true"); - Text autoDownloadTextFalse = Text.translatable("simple_mod_sync.ui.set_sync_screen.auto_download_false"); - AtomicBoolean autoDownload = new AtomicBoolean(Config.instance.getAutoDownload()); - int autoDownloadX = this.width / 2 - 70; - int autoDownloadY = this.getPosY(4); - ButtonWidget auto_download = new ButtonWidget.Builder(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse, (buttonWidget) -> { - autoDownload.set(!autoDownload.get()); - buttonWidget.setMessage(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse); - }).position(autoDownloadX, autoDownloadY).size(140, 20).build(); - this.addDrawableChild(auto_download); - - // Cancel button widget - int cancelButtonX = this.width / 2 - 105; - int cancelButtonY = this.getPosY(5); - ButtonWidget.Builder cancelBuilder = new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.set_sync_screen.cancel_button"), - (buttonWidget) -> { - Config.instance.setDownloadUrl("-"); - MinecraftClient.getInstance().setScreen(parent); - SimpleModSync.StartWorker(); - }); - ButtonWidget cancelButton = cancelBuilder.position(cancelButtonX, cancelButtonY).size(100, 20).build(); - this.addDrawableChild(cancelButton); - - // Set button widget - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.set_sync_screen.confirm_button"), (buttonWidget) -> { - String url = remoteUrl.getText(); - if (!url.isEmpty()) { - Config.instance.setDownloadUrl(url); - MinecraftClient.getInstance().setScreen(parent); - SimpleModSync.StartWorker(); - } - }).position(this.width / 2 + 5, this.getPosY(5)).size(100, 20).build()); - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); - var matrices = context.getMatrices(); - - // Title widget - float scaleModifier = 1.7f; - matrices.push(); - matrices.scale(scaleModifier, scaleModifier, scaleModifier); // Scale the text - Text titleText = Text.translatable("simple_mod_sync.ui.set_sync_screen.title"); - int titleX = (int)(this.width * 0.6f) / 2 - titleText.getString().length() - 33; - int titleY = 30; - context.drawText(this.textRenderer, titleText, titleX, titleY, 0xFF3DF6B4, true); - matrices.pop(); - } - - private int getPosY(int elementIndex) { - return 50 + elementIndex * 25; - } -} diff --git a/src/main/java/dev/oxydien/ui/SyncFullViewScreen.java b/src/main/java/dev/oxydien/ui/SyncFullViewScreen.java deleted file mode 100644 index 5f2ee77..0000000 --- a/src/main/java/dev/oxydien/ui/SyncFullViewScreen.java +++ /dev/null @@ -1,176 +0,0 @@ -package dev.oxydien.ui; - -import dev.oxydien.SimpleModSync; -import dev.oxydien.config.Config; -import dev.oxydien.data.ContentSyncProgress; -import dev.oxydien.data.ProgressCallback; -import dev.oxydien.data.SyncData; -import dev.oxydien.enums.CallbackReason; -import dev.oxydien.ui.widget.ContentSyncProgressWidget; -import dev.oxydien.ui.widget.SimpleBackgroundWidget; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.MultilineTextWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.text.Text; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -public class SyncFullViewScreen extends Screen implements ProgressCallback { - private HashMap progress; - private int page; - private int pageSize; - private final Screen parent; - private SimpleBackgroundWidget progressBar; - private final int widgetHeight = 45; - - public SyncFullViewScreen(Text title, @Nullable Screen parent) { - super(title); - this.progress = new HashMap<>(); - this.page = 0; - this.pageSize = 4; - this.parent = parent; - } - - @Override - public void onDisplayed() { - super.onDisplayed(); - - SimpleModSync.worker.subscribe(this); - } - - @Override - public void init() { - super.init(); - this.progress = new HashMap<>(); - this.pageSize = (this.height - 100) / this.widgetHeight; - - // Back button - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.sync_full_view.back_button"), - (buttonWidget) -> MinecraftClient.getInstance().setScreen(this.parent)).position(3, 5).size(60, 20).build()); - - this.progressBar = new SimpleBackgroundWidget(0, 0, this.width, 2, 0xFFFFFFFF); - this.addDrawableChild(this.progressBar); - - // Title - Text titleText = Text.translatable("simple_mod_sync.ui.sync_full_view.title"); - this.addDrawableChild( - new MultilineTextWidget(this.width / 2 - titleText.getString().length() - 30, 10, titleText, this.textRenderer) - .setTextColor(0xFF3DF6B4)); - - // Url field - TextFieldWidget urlField = new TextFieldWidget(this.textRenderer, this.width / 2 - 150, 24, - 300, 20, Text.literal("")); - urlField.setMaxLength(368); - urlField.setText(Config.instance.getDownloadUrl()); - this.addDrawableChild(urlField); - - // Save Url button - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.sync_full_view.save_url_button"), (buttonWidget) -> { - String url = urlField.getText(); - if (!url.isEmpty()) { - Config.instance.setDownloadUrl(url); - } - }).position(this.width / 2 - 150, 45).size(95, 20).build()); - - // Sync button - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.sync_full_view.sync_button"), - (buttonWidget) -> SimpleModSync.StartWorker()).position(this.width / 2 - 48, 45).size(95, 20).build()); - - // Auto download toggle button widget - AtomicBoolean autoDownload = new AtomicBoolean(Config.instance.getAutoDownload()); - Text autoDownloadTextTrue = Text.translatable("simple_mod_sync.ui.sync_full_view.auto_download_true"); - Text autoDownloadTextFalse = Text.translatable("simple_mod_sync.ui.sync_full_view.auto_download_false"); - ButtonWidget auto_download = new ButtonWidget.Builder(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse, (buttonWidget) -> { - autoDownload.set(!autoDownload.get()); - buttonWidget.setMessage(autoDownload.get() ? autoDownloadTextTrue : autoDownloadTextFalse); - }).position(this.width / 2 + 55, 45).size(95, 20).build(); - this.addDrawableChild(auto_download); - - // Previous and next page buttons - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.sync_full_view.previous_page"), (buttonWidget) -> { - if (this.page > 0) { - this.page--; - } - }).size(20, 20).position(this.width / 2 - 175, 45).build()); - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("simple_mod_sync.ui.sync_full_view.next_page"), (buttonWidget) -> { - int numPages = (int)Math.ceil((float) this.getContentAmount() / (float)this.pageSize); - if (this.page < numPages - 1) { - this.page++; - } - }).size(20, 20).position(this.width / 2 + 155, 45).build()); - - this.onProgress(CallbackReason.NONE); - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); - - int index = 0; - - // Sort by progress then name - List sortedWidgets = new ArrayList<>(progress.values()); - sortedWidgets.sort(Comparator.comparingInt((ContentSyncProgressWidget w) -> - w != null && w.getProgress() == 0 ? Integer.MAX_VALUE : w != null ? w.getProgress() : Integer.MAX_VALUE) - .thenComparing(w -> w != null && w.getName() != null ? w.getName() : "") - ); - - // Set page size based on screen height - this.pageSize = (this.height - 100) / this.widgetHeight; - - // Draw progress widgets - for (ContentSyncProgressWidget widget : sortedWidgets) { - if (widget == null) continue; - // Only draw widgets on the current page - if (index >= page * pageSize && index < (page + 1) * pageSize) { - widget.setPosition(this.width / 2 - widget.getWidth() / 2, - 70 + (index - page * pageSize) * widget.getHeight() + (index - page * pageSize) * 3); - widget.render(context, mouseX, mouseY, delta); - } - - index++; - } - } - - @Override - public void onProgress(CallbackReason reason) { - //SimpleModSync.LOGGER.info("Sync progress: {}", reason); - - this.progressBar.setSize((int)(this.width * ((float)SimpleModSync.worker.GetProgress() / 100f)), 2); - - List progresses = SimpleModSync.worker.GetModProgress(); - SyncData data = SimpleModSync.worker.GetSyncData(); - if (data == null) return; - for (SyncData.Content content : data.getContent()) { - if (content == null) continue; - - ContentSyncProgress modProgress = progresses.stream().filter(p -> p.getIndex() == content.getIndex()).findFirst().orElse(null); - if (!progress.containsKey(content.getIndex())) { - progress.put(content.getIndex(), new ContentSyncProgressWidget(0, 0, 350, this.widgetHeight, this.textRenderer, content, modProgress)); - } - - ContentSyncProgressWidget widget = progress.get(content.getIndex()); - if (widget != null) { - widget.setProgress(modProgress); - } - } - } - - private int getContentAmount() { - SyncData data = SimpleModSync.worker.GetSyncData(); - if (data == null) return 0; - int counter = 0; - for (int i = 0; i < data.getContent().size(); i++) { - if (data.getContent().get(i) != null) counter++; - } - return counter; - } -} diff --git a/src/main/java/dev/oxydien/ui/widget/ContentSyncProgressWidget.java b/src/main/java/dev/oxydien/ui/widget/ContentSyncProgressWidget.java deleted file mode 100644 index 6461aaf..0000000 --- a/src/main/java/dev/oxydien/ui/widget/ContentSyncProgressWidget.java +++ /dev/null @@ -1,208 +0,0 @@ -package dev.oxydien.ui.widget; - -import dev.oxydien.data.ContentSyncProgress; -import dev.oxydien.data.SyncData; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.*; -import net.minecraft.client.gui.navigation.GuiNavigation; -import net.minecraft.client.gui.navigation.GuiNavigationPath; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.render.RenderLayer; -import net.minecraft.text.Text; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -public class ContentSyncProgressWidget implements Widget, Drawable, Element, Selectable { - private int x; - private int y; - private final int width; - private final int height; - private final TextRenderer textRenderer; - private final SyncData.Content content; - @Nullable private ContentSyncProgress progress; - - public ContentSyncProgressWidget(int x, int y, int width, int height, TextRenderer textRenderer, SyncData.Content content, @Nullable ContentSyncProgress progress) { - this.setPosition(x, y); - this.width = width; - this.height = height; - this.textRenderer = textRenderer; - this.content = content; - this.progress = progress; - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - // Background - context.fill(RenderLayer.getGuiOverlay(), this.getX(), this.getY(), this.getX() + this.getWidth(), - this.getY() + this.getHeight(), 0xa0000000); - - // Progress bar - int progressNum = this.progress != null ? this.progress.getProgress() : 0; - if (this.progress != null) { - context.fill(this.getX(), this.getY(), - this.getX() + (int) (this.getWidth() * (float) (progressNum) / (float) 100), this.getY() + 1, 0xa3FFFFFF); - } - - int textHeight = this.getY() + this.getHeight() / 2 - this.textRenderer.fontHeight / 2 + 2; - - // Outcome - String outcomeText = progressNum >= 100 && this.progress != null && !this.progress.isError() ? "COMPLETE" : - (this.progress != null ? this.progress.getOutcome().name() : "AWAITING_WORKER"); - context.drawText(this.textRenderer, Text.translatable(String.format("simple_mod_sync.ui.outcome.%s", outcomeText.toLowerCase())), - this.getX() + this.getWidth() - this.textRenderer.getWidth(outcomeText) - 3, textHeight, 0xFFF13FFF, false); - - // Outcome exception - if (this.progress != null && this.progress.isError() && this.progress.getException() != null) { - context.drawText(this.textRenderer, Text.translatable("simple_mod_sync.ui.error.exception").getString(), - this.getX() + 5, this.getY() + 2, 0xFF55FFFF, false); - } - - // Mod name - String modName = this.content.getName(); - context.drawText(this.textRenderer, String.format("§l%s§r", modName.substring(0, Math.min(22, modName.length()))), - this.getX() + 15, textHeight, 0xFF55FFFF, false); - context.drawText(this.textRenderer, this.content.getType(), this.getX() + 180, - textHeight, 0xFFFFFFFF, false); - } - - public int getProgress() { - return this.progress != null ? this.progress.getProgress() : 0; - } - public String getName() { - return this.content.getName(); - } - - public void setProgress(@Nullable ContentSyncProgress progress) { - this.progress = progress; - } - - @Override - public void mouseMoved(double mouseX, double mouseY) { - Element.super.mouseMoved(mouseX, mouseY); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - return Element.super.mouseClicked(mouseX, mouseY, button); - } - - @Override - public boolean mouseReleased(double mouseX, double mouseY, int button) { - return Element.super.mouseReleased(mouseX, mouseY, button); - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - return Element.super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { - return Element.super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - return Element.super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean keyReleased(int keyCode, int scanCode, int modifiers) { - return Element.super.keyReleased(keyCode, scanCode, modifiers); - } - - @Override - public boolean charTyped(char chr, int modifiers) { - return Element.super.charTyped(chr, modifiers); - } - - @Override - public @Nullable GuiNavigationPath getNavigationPath(GuiNavigation navigation) { - return Element.super.getNavigationPath(navigation); - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return Element.super.isMouseOver(mouseX, mouseY); - } - - @Override - public void setFocused(boolean focused) { - } - - @Override - public boolean isFocused() { - return false; - } - - @Override - public @Nullable GuiNavigationPath getFocusedPath() { - return Element.super.getFocusedPath(); - } - - @Override - public SelectionType getType() { - return SelectionType.NONE; - } - - @Override - public boolean isNarratable() { - return Selectable.super.isNarratable(); - } - - @Override - public void appendNarrations(NarrationMessageBuilder builder) { - } - - @Override - public void setX(int x) { - this.x = x; - } - - @Override - public void setY(int y) { - this.y = y; - } - - @Override - public int getX() { - return this.x; - } - - @Override - public int getY() { - return this.y; - } - - @Override - public int getWidth() { - return this.width; - } - - @Override - public int getHeight() { - return this.height; - } - - @Override - public ScreenRect getNavigationFocus() { - return Widget.super.getNavigationFocus(); - } - - @Override - public void setPosition(int x, int y) { - Widget.super.setPosition(x, y); - } - - @Override - public void forEachChild(Consumer consumer) { - } - - @Override - public int getNavigationOrder() { - return Element.super.getNavigationOrder(); - } -} diff --git a/src/main/java/dev/oxydien/ui/widget/SimpleBackgroundWidget.java b/src/main/java/dev/oxydien/ui/widget/SimpleBackgroundWidget.java deleted file mode 100644 index d052240..0000000 --- a/src/main/java/dev/oxydien/ui/widget/SimpleBackgroundWidget.java +++ /dev/null @@ -1,111 +0,0 @@ -package dev.oxydien.ui.widget; - -import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.render.RenderLayer; - -import java.util.function.Consumer; - -public class SimpleBackgroundWidget implements Widget, Drawable, Element, Selectable { - private int x; - private int y; - private int width; - private int height; - private int color; - - public SimpleBackgroundWidget(int x, int y, int width, int height, int color) { - this.setPosition(x, y); - this.color = (color == 0) ? 0xa0000000 : color; - this.width = width; - this.height = height; - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - context.fill(RenderLayer.getGuiOverlay(), this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), this.color); - } - - @Override - public void setFocused(boolean focused) { - return; - } - - @Override - public boolean isFocused() { - return false; - } - - @Override - public void setX(int x) { - this.x = x; - } - - @Override - public void setY(int y) { - this.y = y; - } - - @Override - public int getX() { - return this.x; - } - - @Override - public int getY() { - return this.y; - } - - @Override - public int getWidth() { - return this.width; - } - - @Override - public int getHeight() { - return this.height; - } - - public int getColor() { - return this.color; - } - - public void setPosition(int x, int y) { - this.x = x; - this.y = y; - } - - public void setSize(int width, int height) { - this.width = width; - this.height = height; - } - - public void setColor(int color) { - this.color = color; - } - - @Override - public ScreenRect getNavigationFocus() { - return Widget.super.getNavigationFocus(); - } - - @Override - public void forEachChild(Consumer consumer) { - - } - - @Override - public SelectionType getType() { - return SelectionType.NONE; - } - - @Override - public boolean isNarratable() { - return Selectable.super.isNarratable(); - } - - @Override - public void appendNarrations(NarrationMessageBuilder builder) { - } -} diff --git a/src/main/java/dev/oxydien/ui/widget/SyncOverviewWidget.java b/src/main/java/dev/oxydien/ui/widget/SyncOverviewWidget.java deleted file mode 100644 index df7e5b6..0000000 --- a/src/main/java/dev/oxydien/ui/widget/SyncOverviewWidget.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.oxydien.ui.widget; - -import dev.oxydien.SimpleModSync; -import dev.oxydien.enums.SyncErrorType; -import dev.oxydien.enums.SyncState; -import dev.oxydien.ui.SyncFullViewScreen; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.PressableWidget; -import net.minecraft.client.render.RenderLayer; -import net.minecraft.text.Text; - -public class SyncOverviewWidget extends PressableWidget { - private final TextRenderer textRenderer; - - public SyncOverviewWidget(TextRenderer textRenderer) { - super(0 ,0, 120, 28, Text.literal("")); - this.textRenderer = textRenderer; - } - - @Override - protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { - super.renderWidget(context, mouseX, mouseY, delta); - SyncState state = SimpleModSync.worker.GetState(); - SyncErrorType errorType = SimpleModSync.worker.GetErrorType(); - int progress = SimpleModSync.worker.GetProgress(); - - int foregroundColor = state == SyncState.ERROR ? 0xFFFF0000 : 0xFFFFFFFF; - int updateColor = state == SyncState.ERROR ? 0xA3FF0000 : 0xA3FFFFFF; - - // Background - context.fill(RenderLayer.getGui(), this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), 0xa0000000); - - // Progress bar - context.fill(RenderLayer.getGuiOverlay(), this.getX() + 4, this.getY() + 2, - this.getX() + (int)(this.getWidth() * (float)(progress) / (float)100) - 2, this.getY() + 5, updateColor); - - // Text - Text text; - if (state != SyncState.ERROR) text = Text.translatable(String.format("simple_mod_sync.ui.%s", state.name().toLowerCase()), progress); - else text = Text.translatable(String.format("simple_mod_sync.ui.error.%s", errorType.name().toLowerCase())); - - int i = 0; - for (String line : text.getString().split("\n")) { - context.drawText(this.textRenderer, line, this.getX() + 5, this.getY() + 6 + (this.textRenderer.fontHeight + 2) * i++, foregroundColor, false); - } - } - - @Override - public void onPress() { - MinecraftClient.getInstance().setScreen(new SyncFullViewScreen(Text.translatable("simple_mod_sync.ui.sync_full_view.title"), null)); - } - - @Override - protected void appendClickableNarrations(NarrationMessageBuilder builder) { - } -} diff --git a/src/main/java/dev/oxydien/utils/FileUtils.java b/src/main/java/dev/oxydien/utils/FileUtils.java deleted file mode 100644 index 9845a9a..0000000 --- a/src/main/java/dev/oxydien/utils/FileUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.oxydien.utils; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipFile; - -public class FileUtils { - public static String ReadFile(String path) throws IOException { - Path filePath = Paths.get(path); - List lines = Files.readAllLines(filePath); - return String.join("\n", lines); - } - - public static void WriteFile(String path, String content) throws IOException { - Path filePath = Paths.get(path); - Files.write(filePath, content.getBytes()); - } - - public static List UnZipFile(String zipFilePath, String destinationDirectory) throws IOException { - List extractedFiles = new ArrayList<>(); - try (ZipFile zipFile = new ZipFile(zipFilePath)) { - zipFile.entries().asIterator().forEachRemaining(zipEntry -> { - try { - if (zipEntry.isDirectory()) { - Path dir = Paths.get(destinationDirectory, zipEntry.getName()); - Files.createDirectories(dir); - extractedFiles.add(dir.toAbsolutePath()); - } else { - Path file = Paths.get(destinationDirectory, zipEntry.getName()); - Files.createDirectories(file.getParent()); - Files.copy(zipFile.getInputStream(zipEntry), file); - extractedFiles.add(file.toAbsolutePath()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - return extractedFiles; - } - - /** - * Returns a list of all file paths under the given directory, recursively. - * @param startPath The root directory path to start searching from - * @return List of absolute file paths as strings - */ - public static List GetFilePaths(String startPath) { - List filePaths = new ArrayList<>(); - File startDir = new File(startPath); - - if (!startDir.exists() || !startDir.isDirectory()) { - throw new IllegalArgumentException("Invalid directory path: " + startPath); - } - - collectFilePaths(startDir, filePaths); - return filePaths; - } - - private static void collectFilePaths(File directory, List filePaths) { - File[] files = directory.listFiles(); - if (files != null) { - for (File file : files) { - filePaths.add(file.getAbsolutePath()); - if (file.isDirectory()) { - collectFilePaths(file, filePaths); - } - } - } - } -} diff --git a/src/main/java/dev/oxydien/utils/PathUtils.java b/src/main/java/dev/oxydien/utils/PathUtils.java deleted file mode 100644 index ae58d84..0000000 --- a/src/main/java/dev/oxydien/utils/PathUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.oxydien.utils; - -import dev.oxydien.logger.Log; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class PathUtils { - - @Nullable - public static Path PathExistsFromStartInDir(String dirPath, String search) { - Path dir = Path.of(dirPath); - if (Files.isDirectory(dir)) { - try (var stream = Files.list(dir)) { - for (var p : stream.toList()) { - if (p.getFileName().toString().startsWith(search)) { - return p; - } - } - } catch (IOException e) { - Log.Log.error("path-utils.pefsid.IOException", "Error while searching for file in directory", e); - } - } - return null; - } - - public static boolean PathExists(String path) { - return Files.exists(Path.of(path)); - } - - public static void CreateFolder(String path) { - try { - Files.createDirectories(Path.of(path)); - } catch (IOException e) { - Log.Log.error("path-utils.create-folder.IOException", "Error while creating folder", e); - } - } - - /** - * Sanitizes a given path by making sure it is a valid path and does not attempt to traverse outside the given base directory. - * Used so the mod cannot access files outside the base (minecraft) directory. - * - * @param basePath The base directory to work from. - * @param userProvidedPath The path specified by the user. - * @return The sanitized path. - * @throws SecurityException If the path is invalid or attempts to traverse outside the base directory. - */ - public static String sanitizePath(String basePath, String userProvidedPath) { - try { - // Convert paths to canonical form - Path baseDir = Paths.get(basePath).toAbsolutePath().normalize(); - Path requestedPath = baseDir.resolve(userProvidedPath).toAbsolutePath().normalize(); - - // Check if the requested path starts with the base directory - if (!requestedPath.startsWith(baseDir)) { - throw new SecurityException("Path traversal attempt detected"); - } - - return requestedPath.toString(); - } catch (Exception e) { - throw new SecurityException("Invalid path", e); - } - } -} diff --git a/src/main/java/dev/oxydien/utils/StringUtils.java b/src/main/java/dev/oxydien/utils/StringUtils.java deleted file mode 100644 index f6df97e..0000000 --- a/src/main/java/dev/oxydien/utils/StringUtils.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.oxydien.utils; - -public class StringUtils { - public static String removeUnwantedCharacters(String input) { - return input.replaceAll("[^a-zA-Z0-9.\\-_]", ""); - } -} diff --git a/src/main/java/dev/oxydien/workers/ModDownloadWorker.java b/src/main/java/dev/oxydien/workers/ModDownloadWorker.java deleted file mode 100644 index 23e932b..0000000 --- a/src/main/java/dev/oxydien/workers/ModDownloadWorker.java +++ /dev/null @@ -1,584 +0,0 @@ -package dev.oxydien.workers; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import dev.oxydien.data.ConfigData; -import dev.oxydien.data.ContentSyncProgress; -import dev.oxydien.enums.*; -import dev.oxydien.logger.Log; -import dev.oxydien.networking.FileDownloader; -import dev.oxydien.config.Config; -import dev.oxydien.data.ProgressCallback; -import dev.oxydien.data.SyncData; -import dev.oxydien.utils.FileUtils; -import dev.oxydien.utils.PathUtils; -import dev.oxydien.utils.StringUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; - -public class ModDownloadWorker implements Runnable { - /// State where the worker is - private SyncState state; - /// Data used for syncing - @Nullable private SyncData syncData; - /// Error type if there is one else [SyncErrorType::NONE] - private SyncErrorType errorType; - /// Progress of the sync (0 to 100) - private final AtomicInteger overallProgress; - /// Callbacks to notify when progress is updated - public static List callbacks = new CopyOnWriteArrayList<>(); - /// Main worker thread - private final AtomicReference workerThread; - /// Progress of each content separately - private final AtomicReference> contentSyncProgress; - /// Executor service (waits for completion) - private CompletionService completionService; - /// Executor service (runs tasks) - private ExecutorService executorService; - - public SyncState GetState() { - return this.state; - } - - public SyncErrorType GetErrorType() { - return this.errorType; - } - - public int GetProgress() { - return this.overallProgress.get(); - } - - public SyncData GetSyncData() { - return this.syncData; - } - - public List GetModProgress() { - return this.contentSyncProgress.get(); - } - - public ModDownloadWorker() { - this.state = SyncState.DID_NOT_SYNC; - this.syncData = null; - this.errorType = SyncErrorType.NONE; - this.workerThread = new AtomicReference<>(); - this.overallProgress = new AtomicInteger(0); - this.contentSyncProgress = new AtomicReference<>(new CopyOnWriteArrayList<>()); - this.executorService = Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors(), 4)); - this.completionService = new ExecutorCompletionService<>(this.executorService); - } - - public void subscribe(ProgressCallback callback) { - if (callback != null && !callbacks.contains(callback)) { - callbacks.add(callback); - //SimpleModSync.LOGGER.info("[SMS-WORKER] Added callback {} {}", callback, callbacks); - } - } - - public void unsubscribe(ProgressCallback callback) { - if (callback != null) { - callbacks.remove(callback); - } - } - - /** - * Starts the mod download worker. This will: - * - Check if the remote URL is set, and if not, set an error and return - * - Check if the remote URL is set to "-", in which case the synchronization will be disabled - * - Download the remote JSON file - * - Parse the JSON file - * - Download all mods listed in the JSON file - * - Set the state to READY or NEEDS_RESTART depending on whether any mods were downloaded - */ - @Override - public void run() { - workerThread.set(Thread.currentThread()); - Log.Log.info("bw.run", "Mod download worker started"); - - // Check if the remote URL is set - String url = Config.instance.getDownloadUrl(); - if (url.isEmpty()) { - this.handleError(SyncErrorType.REMOTE_NOT_SET, "Remote URL not set"); - return; - } - - // Check if the remote URL is set to "-", in which case the synchronization will be disabled - if (url.equals("-")) { - this.overallProgress.set(100); - this.errorType = SyncErrorType.REMOTE_NOT_SET; - this.setState(SyncState.READY); - Log.Log.info("bw.run", "Synchronization disabled, returning early"); - return; - } - - // Download the remote JSON file - this.updateProgress(2); - this.setState(SyncState.CHECKING_REMOTE); - - String jsonString; - try { - jsonString = FileDownloader.downloadString(url); - } catch (IOException | URISyntaxException e) { - this.handleError(SyncErrorType.REMOTE_NOT_FOUND, "Remote URL not found", e); - return; - } - - // Parse the JSON file - this.updateProgress(4); - this.setState(SyncState.PARSING_REMOTE); - try { - this.syncData = this.parseSyncData(jsonString); - } catch (Exception e) { - this.handleError(SyncErrorType.PARSING_FAILED, "Failed to parse remote data", e); - return; - } - - // Download all the content - this.updateProgress(10); - this.setState(SyncState.DOWNLOADING); - - int totalTasks = this.syncData.getContent().size(); - for (SyncData.Content content : this.syncData.getContent()) { - this.completionService.submit(() -> switch (content.getType()) { - case "config" -> this.downloadConfig(content); - case null, default -> this.downloadContent(content); - }); - } - - // Wait for completion - boolean changed = false; - for (int i = 0; i < totalTasks; i++) { - try { - Future future = this.completionService.take(); - changed |= future.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - Log.Log.error("bw.run.interruptException", "Download process was interrupted for {}", - this.syncData.getContent().get(i).getName(), e); - break; - } catch (ExecutionException e) { - Log.Log.error("bw.run.executionException", "Error during parallel download for {}", - this.syncData.getContent().get(i).getName(), e); - } - } - - // Shutdown executor service and wait for termination - this.executorService.shutdown(); - try { - if (!this.executorService.awaitTermination(60, TimeUnit.SECONDS)) { - this.executorService.shutdownNow(); - } - } catch (InterruptedException e) { - this.executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - this.updateProgress(98); - - // Run modifications - this.setState(SyncState.MODIFICATIONS); - for (SyncData.Modification modification : this.syncData.getModify()) { - this.runModification(modification); - } - - // Update progress - this.updateProgress(100); - if (changed) { - this.setState(SyncState.NEEDS_RESTART); - } else { - this.setState(SyncState.READY); - } - - Log.Log.info("bw.run", "Synchronization finished"); - } - - /** - * Downloads a mod from the given content URL to the download destination. - * - *

If the file already exists, it will be skipped. If an older version of the mod exists, it will be deleted. - * This method will catch any exceptions that occur during the download process and return false. - * - * @param content The content to download. - * @return Whether the mod was downloaded successfully. - */ - private boolean downloadContent(SyncData.Content content) { - // Validate content type - if (Objects.equals(content.getType(), "config")) { - Log.Log.warn("bw.downloadContent","DEV ISSUE: config is not supported in this function, skipping {}", content.getName()); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUSPICIOUS_CONTENT, null); - return false; - } - - // Get working folder - var workingDirectory = this.getContentValidDirectory(content.getDirectoryOverride(), content.getTypeFolder()); - if (workingDirectory.isEmpty()) { - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUSPICIOUS_CONTENT, null); - return false; - } - - var contentName = StringUtils.removeUnwantedCharacters(content.getName()); - var contentVersion = StringUtils.removeUnwantedCharacters(content.getVersion()); - - // Create full path to the content - String path = workingDirectory + "/" + - contentName + "-" + - contentVersion + - content.getFileExtension(); - - // Check if content already exists - if (FileDownloader.fileExists(path)) { - Log.Log.debug("bw.downloadContent","File already exists, skipping {}", content.getName()); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.ALREADY_EXISTS, null); - return false; - } - - // Find any older versions - Path olderVersion = PathUtils.PathExistsFromStartInDir(workingDirectory + "/", contentName); - if (olderVersion != null) { - Log.Log.debug("bw.downloadContent", "Found older version of {}, deleting {}", content.getName(), olderVersion.getFileName()); - try { - Files.delete(olderVersion); - } catch (IOException e) { - Log.Log.error("bw.downloadContent.delete.IOException","Failed to delete file", e); - } - } - - // Download the content - Log.Log.debug("bw.downloadContent", "Downloading {} {}", content.getName(), content.getVersion()); - try { - FileDownloader.downloadFileWithProgress(content.getUrl(), path, - (progress) -> this.updateContentProgress(content.getIndex(), progress, ContentSyncOutcome.IN_PROGRESS, null)); - } catch (IOException e) { - Log.Log.error("bw.downloadContent.write.IOException", "Failed to download file {}", content.getName(), e); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.DOWNLOAD_INTERRUPTED , e); - return false; - } catch (URISyntaxException e) { - Log.Log.error("bw.downloadContent.write.URISyntaxException", "Failed to download file {}", content.getName(), e); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.INVALID_URL , e); - return false; - } - - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUCCESS, null); - Log.Log.debug("bw.downloadContent", "Successfully Downloaded {} {} {}", content.getName(), content.getType(), content.getVersion()); - return true; - } - - /** - * Downloads a config file from the given url and places it in the valid directory specified by {@link #getContentValidDirectory(String, String)}. - *

- * This function will check if the config already exists, and if so, will skip the download and return false. - * If the config is an older version, it will delete the older version and apply the changes from the newer version. - * If the config is a newer version, it will overwrite the existing config. - *

- * The progress of the download will be updated in the callback ({@link #updateContentProgress(int, int, ContentSyncOutcome, Exception)}). - * @param content the (config) content to download - * @return true if the config was successfully downloaded or updated, false if not - */ - private boolean downloadConfig(SyncData.Content content) { - // Validate content type - if (!Objects.equals(content.getType(), "config")) { - Log.Log.warn("bw.downloadConfig","DEV ISSUE: {} is not supported in this function, skipping {}", content.getType(), content.getName()); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUSPICIOUS_CONTENT, null); - return false; - } - - // Get working folder - var workingDirectory = this.getContentValidDirectory(content.getDirectoryOverride(), content.getTypeFolder()); - if (workingDirectory.isEmpty()) { - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUSPICIOUS_CONTENT, null); - return false; - } - - var configName = StringUtils.removeUnwantedCharacters(content.getName()); - var configVersion = StringUtils.removeUnwantedCharacters(content.getVersion()); - - // Create full path to the config registry (sms_configName-version.json) - String path = workingDirectory + "/" + "sms_" + - configName + "-" + - configVersion + - ".json"; - var tempZipPath = workingDirectory + "/" + "sms_" + - configName + "-" + - configVersion + - ".archive.zip"; - - // Check if config already exists - if (FileDownloader.fileExists(path)) { - Log.Log.debug("bw.downloadConfig","Found existing config, skipping {}", content.getName()); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.ALREADY_EXISTS, null); - return false; - } - this.updateContentProgress(content.getIndex(), 10, ContentSyncOutcome.ALREADY_EXISTS, null); - - // Check for older versions - Path olderVersion = PathUtils.PathExistsFromStartInDir(workingDirectory + "/", configName); - if (olderVersion != null) { - // Remove older version's changes - Log.Log.debug("bw.downloadConfig", "Found older version of {}, deleting changes {}", configName, olderVersion.getFileName()); - boolean allowed = true; - try { - var configJsonStr = FileUtils.ReadFile(olderVersion.toString()); - var jsonElement = JsonParser.parseString(configJsonStr); - var jsonObject = jsonElement.getAsJsonObject(); - var configData = ConfigData.fromJson(jsonObject); - - for (String file : configData.getConfig()) { - try { - Files.delete(Path.of(file)); - } catch (IOException e) { - Log.Log.warn("bw.downloadConfig.delete.IOException","Failed to delete file, ignoring: ", e); - } - } - } catch (IOException e) { - Log.Log.error("bw.downloadConfig.read.IOException", "Failed to read file", e); - allowed = false; - } - - if (allowed) { - try { - Files.delete(olderVersion); - } catch (IOException e) { - Log.Log.error("bw.downloadConfig.delete.IOException","Failed to delete file", e); - } - } - } - - // Download the config - Log.Log.debug("bw.downloadConfig", "Downloading {} {}", content.getName(), content.getVersion()); - try { - FileDownloader.downloadFileWithProgress(content.getUrl(), tempZipPath, - (progress) -> this.updateContentProgress(content.getIndex(), (int) (10 + (progress * 0.8)), ContentSyncOutcome.IN_PROGRESS, null)); - var modifiedFiles = FileUtils.UnZipFile(tempZipPath, workingDirectory); - var modifiedFilesAsString = modifiedFiles.stream().map(Path::toString).toList(); - var configData = new ConfigData(modifiedFilesAsString); - var configJsonStr = configData.toJson().toString(); - FileUtils.WriteFile(path, configJsonStr); - - // Remove temp zip file - Files.delete(Path.of(tempZipPath)); - } catch (IOException e) { - Log.Log.error("bw.downloadConfig.write.IOException", "Failed to download parse or write file {}", content.getName(), e); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.DOWNLOAD_INTERRUPTED , e); - return false; - } catch (URISyntaxException e) { - Log.Log.error("bw.downloadConfig.write.URISyntaxException", "Failed to download file {}", content.getName(), e); - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.INVALID_URL , e); - return false; - } - - this.updateContentProgress(content.getIndex(), 100, ContentSyncOutcome.SUCCESS, null); - Log.Log.debug("bw.downloadConfig", "Successfully Downloaded {} {} {}", content.getName(), content.getType(), content.getVersion()); - - return true; - } - - private void runModification(SyncData.Modification modification) { - // Validate input - if (modification.getType() == SyncModificationType.RENAME && modification.getResult() == null) { - Log.Log.error("bw.runModification", "Modification type is RENAME but result is null, (result is used for RENAME)"); - return; - } - - // Get working folder - var workingDirectory = this.getContentValidDirectory(modification.getPath(), ""); - if (workingDirectory.isEmpty()) { - Log.Log.debug("bw.runModification", "Suspicious content, skipping {} {}", modification.getType(), modification.getPath()); - return; - } - - var matchData = FileUtils.GetFilePaths(workingDirectory); - if (matchData.isEmpty()) { - Log.Log.debug("bw.runModification", "Ignoring content, no data for {} {}", modification.getType(), modification.getPath()); - return; - } - - List relativeMatches = new ArrayList<>(); - for (String match : matchData) { - String relativePath = Path.of(workingDirectory).relativize(Path.of(match)).toString(); - relativeMatches.add(relativePath); - } - - List matches = new ArrayList<>(); - var pattern = Pattern.compile(modification.getPattern()); - for (var filePath : relativeMatches) { - var matcher = pattern.matcher(filePath); - if (matcher.matches()) { - matches.add(filePath); - Log.Log.debug("bw.runModification", "Found match for {} at {}", modification.getPattern(), filePath); - break; - } - } - - switch (modification.getType()) { - case REMOVE: - for (var match : matches) { - try { - Files.delete(Path.of(match)); - } catch (IOException e) { - Log.Log.error("bw.runModification.delete.IOException","Failed to delete file", e); - } - } - break; - case RENAME: - for (var match : matches) { - try { - assert modification.getResult() != null; - var targetFile = Path.of(match); - var parent = targetFile.getParent(); - Files.move(targetFile, Path.of(parent + "/" + modification.getResult())); - } catch (IOException e) { - Log.Log.error("bw.runModification.move.IOException","Failed to move file", e); - } - } - break; - default: - Log.Log.error("bw.runModification", "Unknown modification type {}", modification.getType()); - } - Log.Log.debug("bw.runModification", "Successfully ran modification {} at {}", modification.getType(), workingDirectory); - } - - /** - * Updates the progress of the content at the given index in the mod progress list. - * If the content doesn't exist, a new ContentSyncProgress object is created and added to the list. - * If the content does exist, its progress is updated. - * The overall progress is then updated by calling {@link #updateOverallProgress()}. - * - * @param contentIndex The index of the content to update. - * @param progress The new progress of the content. - * @param outcome The outcome of the content. If the content has finished downloading, this is the outcome. - * @param e The exception that occurred during the download, if any. - */ - private void updateContentProgress(int contentIndex, int progress, ContentSyncOutcome outcome, @Nullable Exception e) { - List contentSyncProgress = this.contentSyncProgress.get(); - // Find existing content with the same index - ContentSyncProgress content = contentSyncProgress.stream() - .filter(mod -> mod.getIndex() == contentIndex) - .findFirst().orElse(null); - - if (content == null) { - // Create new content - ContentSyncProgress newContent = new ContentSyncProgress(contentIndex, progress); - if (errorType != null) { - newContent.setOutcome(outcome, e); - } - contentSyncProgress.add(newContent); - } else { - // Update existing content's progress - content.setProgress(progress); - if (errorType != null) { - content.setOutcome(outcome, e); - } - } - this.contentSyncProgress.set(contentSyncProgress); - this.updateOverallProgress(); - } - - /** - * Calculates the overall progress by summing the progress of all content and dividing it by the total number of content. - * The overall progress is then set to the calculated value. - * The state is then set to the current state to trigger a progress update callback. - */ - private void updateOverallProgress() { - int totalProgress = (int) ((float) this.contentSyncProgress.get().stream().mapToInt(ContentSyncProgress::getProgress).sum() * 0.95f); - int overallProgress = totalProgress / this.contentSyncProgress.get().size(); - this.overallProgress.set(overallProgress); - this.setState(this.state); // Trigger progress update callback - } - - private void handleError(SyncErrorType errorType, String message) { - this.handleError(errorType, message, null); - } - - private void handleError(SyncErrorType errorType, String message, @Nullable Exception e) { - this.errorType = errorType; - this.setState(SyncState.ERROR); - if (e != null) { - Log.Log.error("bw", "{}", message, e); - } else { - Log.Log.error("bw", "{}", message); - } - } - - private String getContentValidDirectory(@Nullable String directory, String fallback) { - String output; - var baseDir = Config.instance.getDownloadDestination(); - if (directory != null) { - try { - output = PathUtils.sanitizePath(baseDir, directory); - } catch (SecurityException e) { - Log.Log.error("bw.downloadContent.sanitizePath.SecurityException", "Failed to sanitize path", e); - return ""; - } - } else { - if (fallback.isEmpty()) { - return ""; - } - - output = baseDir + "/" + fallback; - } - - if (!PathUtils.PathExists(output)) { - PathUtils.CreateFolder(output); - } - - return output; - } - - private void updateProgress(int progress) { - this.overallProgress.set(progress); - this.setState(state); // Trigger progress update callback - } - - private SyncData parseSyncData(String jsonString) { - JsonElement jsonElement = JsonParser.parseString(jsonString); - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - return SyncData.fromJson(jsonObject); - } - - private void setState(SyncState state) { - this.state = state; - //SimpleModSync.LOGGER.info("[SMS-WORKER] Calling UPDATE callback {}", callbacks); - for (ProgressCallback progressCallback : callbacks) { - progressCallback.onProgress(CallbackReason.UPDATE); - } - } - - /** - * Starts the content download worker. - * This will: - * - Clear any stored data - * - Initialize the executor service - * - Set the state to INITIALIZING - * - Start the worker thread - */ - public void start() { - Thread thread = new Thread(this); - this.syncData = null; - this.contentSyncProgress.set(new CopyOnWriteArrayList<>()); - this.overallProgress.set(0); - this.errorType = null; - this.executorService = Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors(), 4)); - this.completionService = new ExecutorCompletionService<>(this.executorService); - this.setState(SyncState.INITIALIZING); - this.workerThread.set(thread); - thread.start(); - } - - public void stop() { - Thread thread = workerThread.get(); - if (thread != null) { - thread.interrupt(); - } - } -} diff --git a/src/main/resources/assets/simple-mod-sync/lang/cs_cz.json b/src/main/resources/assets/simple-mod-sync/lang/cs_cz.json deleted file mode 100644 index c9e0e63..0000000 --- a/src/main/resources/assets/simple-mod-sync/lang/cs_cz.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "simple_mod_sync.ui.initializing": "Inicializace", - "simple_mod_sync.ui.checking_remote": "Kontroluji server", - "simple_mod_sync.ui.parsing_remote": "Čtení dat ze serveru", - "simple_mod_sync.ui.downloading": "Stahování %d%%...", - "simple_mod_sync.ui.modifications": "Modifikace", - "simple_mod_sync.ui.ready": "Připraveno k hraní", - "simple_mod_sync.ui.needs_restart": "Restartujte hru", - "simple_mod_sync.ui.error": "Chyba", - "simple_mod_sync.ui.error.unknown": "Neznámá chyba", - "simple_mod_sync.ui.error.remote_not_set": "Adresa serveru není nastavena\nNastavte ji v config souboru", - "simple_mod_sync.ui.error.remote_not_found": "Adresa serveru nenalezena\nNepodařilo se připojit k serveru", - "simple_mod_sync.ui.error.parsing_failed": "Nepodařilo se přečíst data ze serveru", - "simple_mod_sync.ui.error.download_failed": "Nepodařilo se stáhnout mody", - "simple_mod_sync.ui.error.failed_to_write_file": "Nepodařilo se zapsat mody na disk", - "simple_mod_sync.ui.set_sync_screen.title": "§lSimple Mod Sync", - "simple_mod_sync.ui.set_sync_screen.subtitle": "§oNastavit adresu serveru", - "simple_mod_sync.ui.set_sync_screen.confirm_button": "Potvrdit", - "simple_mod_sync.ui.set_sync_screen.cancel_button": "Zrušit", - "simple_mod_sync.ui.set_sync_screen.auto_download_true": "Vypnout auto-stahování", - "simple_mod_sync.ui.set_sync_screen.auto_download_false": "Zapnout auto-stahování", - "simple_mod_sync.ui.sync_full_view.title": "§lSimple Mod Sync", - "simple_mod_sync.ui.sync_full_view.save_url_button": "Uložit URL", - "simple_mod_sync.ui.sync_full_view.sync_button": "Synchronizovat", - "simple_mod_sync.ui.sync_full_view.auto_download_true": "Vypnout auto", - "simple_mod_sync.ui.sync_full_view.auto_download_false": "Zapnout auto", - "simple_mod_sync.ui.sync_full_view.previous_page": "<", - "simple_mod_sync.ui.sync_full_view.next_page": ">", - "simple_mod_sync.ui.sync_full_view.back_button": "Zpět", - "simple_mod_sync.ui.did_not_sync": "Auto-stahovaní\nvypnuto", - "simple_mod_sync.ui.outcome.success": "Dokončeno", - "simple_mod_sync.ui.outcome.in_progress": "Pracuji", - "simple_mod_sync.ui.outcome.already_exists": "Už existuje", - "simple_mod_sync.ui.outcome.invalid_url": "Špatná URL", - "simple_mod_sync.ui.outcome.download_interrupted": "Stahování přerušeno", - "simple_mod_sync.ui.outcome.awaiting_worker": "Cekám na worker", - "simple_mod_sync.ui.outcome.suspicious_content": "Podezřelý obsah", - "simple_mod_sync.ui.outcome.complete": "Dokončeno!", - "simple_mod_sync.ui.error.exception": "Akce selhala, zkontrolujte logy" -} diff --git a/src/main/resources/assets/simple-mod-sync/lang/en_us.json b/src/main/resources/assets/simple-mod-sync/lang/en_us.json deleted file mode 100644 index 7316cf7..0000000 --- a/src/main/resources/assets/simple-mod-sync/lang/en_us.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "simple_mod_sync.ui.set_sync_screen.title": "§lSimple Mod Sync", - "simple_mod_sync.ui.set_sync_screen.subtitle": "§oSet the remote URL", - "simple_mod_sync.ui.set_sync_screen.confirm_button": "Confirm", - "simple_mod_sync.ui.set_sync_screen.cancel_button": "Cancel", - "simple_mod_sync.ui.set_sync_screen.auto_download_true": "Disable auto download", - "simple_mod_sync.ui.set_sync_screen.auto_download_false": "Enable auto download", - "simple_mod_sync.ui.sync_full_view.title": "§lSimple Mod Sync", - "simple_mod_sync.ui.sync_full_view.save_url_button": "Save URL", - "simple_mod_sync.ui.sync_full_view.sync_button": "Sync now", - "simple_mod_sync.ui.sync_full_view.auto_download_true": "Disable auto", - "simple_mod_sync.ui.sync_full_view.auto_download_false": "Enable auto", - "simple_mod_sync.ui.sync_full_view.previous_page": "<", - "simple_mod_sync.ui.sync_full_view.next_page": ">", - "simple_mod_sync.ui.sync_full_view.back_button": "Back", - "simple_mod_sync.ui.initializing": "Initializing", - "simple_mod_sync.ui.checking_remote": "Checking remote", - "simple_mod_sync.ui.parsing_remote": "Reading remote data", - "simple_mod_sync.ui.did_not_sync": "Auto download\ndisabled", - "simple_mod_sync.ui.downloading": "Downloading %d%%...", - "simple_mod_sync.ui.modifications": "Modifications", - "simple_mod_sync.ui.ready": "Ready to play", - "simple_mod_sync.ui.needs_restart": "Restart your game", - "simple_mod_sync.ui.outcome.success": "Success", - "simple_mod_sync.ui.outcome.in_progress": "In progress", - "simple_mod_sync.ui.outcome.already_exists": "Already exists", - "simple_mod_sync.ui.outcome.invalid_url": "Invalid URL", - "simple_mod_sync.ui.outcome.download_interrupted": "Download failed", - "simple_mod_sync.ui.outcome.awaiting_worker": "Awaiting worker", - "simple_mod_sync.ui.outcome.suspicious_content": "Suspicious content", - "simple_mod_sync.ui.outcome.complete": "Done!", - "simple_mod_sync.ui.error": "Error", - "simple_mod_sync.ui.error.unknown": "Unknown error", - "simple_mod_sync.ui.error.remote_not_set": "Remote URL not set\nSet it in the config file", - "simple_mod_sync.ui.error.remote_not_found": "Remote URL not found\nCould not connect to the server", - "simple_mod_sync.ui.error.parsing_failed": "Failed to parse remote data", - "simple_mod_sync.ui.error.download_failed": "Failed to download mods", - "simple_mod_sync.ui.error.failed_to_write_file": "Failed to write mods to disk", - "simple_mod_sync.ui.error.exception": "Action failed, check logs for details" -} diff --git a/src/main/resources/assets/simple-mod-sync/lang/nl_nl.json b/src/main/resources/assets/simple-mod-sync/lang/nl_nl.json deleted file mode 100644 index a9bdd5d..0000000 --- a/src/main/resources/assets/simple-mod-sync/lang/nl_nl.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "simple_mod_sync.ui.checking_remote": "De server controleren", - "simple_mod_sync.ui.did_not_sync": "Auto-downloaden\nuitgeschakeld", - "simple_mod_sync.ui.downloading": "Downloaden %d%%...", - "simple_mod_sync.ui.error": "Fout", - "simple_mod_sync.ui.error.download_failed": "Downloaden van mods mislukt", - "simple_mod_sync.ui.error.exception": "Actie mislukt, controleer logboeken voor details", - "simple_mod_sync.ui.error.failed_to_write_file": "Mislukt bij het schrijven van mods naar schijf", - "simple_mod_sync.ui.error.parsing_failed": "Mislukt bij het lezen van gegevens van de server", - "simple_mod_sync.ui.error.remote_not_found": "Serveradres niet gevonden\nVerbinding met server mislukt", - "simple_mod_sync.ui.error.remote_not_set": "Serveradres is niet ingesteld\nStel het in in het configuratiebestand", - "simple_mod_sync.ui.error.unknown": "Onbekende fout", - "simple_mod_sync.ui.initializing": "Initialiseren", - "simple_mod_sync.ui.modifications": "Wijzigingen", - "simple_mod_sync.ui.needs_restart": "Herstart je spel", - "simple_mod_sync.ui.outcome.already_exists": "Bestaat al", - "simple_mod_sync.ui.outcome.awaiting_worker": "Wachtende werknemer", - "simple_mod_sync.ui.outcome.complete": "Gedaan!", - "simple_mod_sync.ui.outcome.download_interrupted": "Downloaden mislukt", - "simple_mod_sync.ui.outcome.in_progress": "In uitvoering", - "simple_mod_sync.ui.outcome.invalid_url": "Ongeldige URL", - "simple_mod_sync.ui.outcome.suspicious_content": "Verdachte inhoud", - "simple_mod_sync.ui.outcome.success": "Gedaan", - "simple_mod_sync.ui.parsing_remote": "Gegevens van de server lezen", - "simple_mod_sync.ui.ready": "Klaar om te spelen", - "simple_mod_sync.ui.set_sync_screen.auto_download_false": "Auto-downloaden inschakelen", - "simple_mod_sync.ui.set_sync_screen.auto_download_true": "Auto-downloaden uitschakelen", - "simple_mod_sync.ui.set_sync_screen.cancel_button": "Annuleren", - "simple_mod_sync.ui.set_sync_screen.confirm_button": "Bevestigen", - "simple_mod_sync.ui.set_sync_screen.subtitle": "§oStel het serveradres in", - "simple_mod_sync.ui.set_sync_screen.title": "§lSimple Mod Sync", - "simple_mod_sync.ui.sync_full_view.auto_download_false": "inschakelen auto", - "simple_mod_sync.ui.sync_full_view.auto_download_true": "uitschakelen auto", - "simple_mod_sync.ui.sync_full_view.back_button": "Terug", - "simple_mod_sync.ui.sync_full_view.next_page": ">", - "simple_mod_sync.ui.sync_full_view.previous_page": "<", - "simple_mod_sync.ui.sync_full_view.save_url_button": "URL opslaan", - "simple_mod_sync.ui.sync_full_view.sync_button": "Nu synchroniseren", - "simple_mod_sync.ui.sync_full_view.title": "§lSimple Mod Sync" -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json deleted file mode 100644 index 93f7ad7..0000000 --- a/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "schemaVersion": 1, - "id": "simple-mod-sync", - "version": "${version}", - "name": "Simple Mod Sync", - "description": "A lightweight mod for synchronizing game mods via a URL-based schema.", - "authors": [ - "oxydien" - ], - "contact": { - "homepage": "https://modrinth.com/mod/simple-mod-sync", - "sources": "https://github.com/oxydien/simple-mod-sync" - }, - "license": "MIT", - "icon": "assets/simple-mod-sync/icon.png", - "environment": "*", - "entrypoints": { - "main": [ - "dev.oxydien.SimpleModSync" - ], - "fabric-datagen": [ - "dev.oxydien.SimpleModSyncDataGenerator" - ] - }, - "mixins": [ - "simple-mod-sync.mixins.json" - ], - "depends": { - "fabricloader": ">=0.16.5", - "minecraft": "~1.21.5", - "fabric-api": "*", - "java": ">=21" - } -}