├── .gitignore
├── LICENSE
├── README.md
├── descriptions
├── alextheregent-mod-installer-description.json
├── default-mod-installer-description.json
├── findarkside-mod-installer-description.json
├── lucasio6-mod-installer-description.json
├── mikeypdog-mod-installer-description.json
├── nbakulev-mod-installer-description.json
├── shieldheart-mod-installer-description.json
├── uberfox-mod-installer-description.json
├── wulfmarius-mod-installer-description.json
├── xpazeman-mod-installer-description.json
└── zeobviouslyfakeacc-mod-installer-description.json
├── images
├── installation-directory.png
├── installer-01.png
└── update-available.png
├── pom.xml
├── src
├── main
│ ├── java
│ │ └── me
│ │ │ └── wulfmarius
│ │ │ └── modinstaller
│ │ │ ├── AbortException.java
│ │ │ ├── Asset.java
│ │ │ ├── DependencyResolver.java
│ │ │ ├── InstallationsChangedListener.java
│ │ │ ├── InstallationsChangedListeners.java
│ │ │ ├── Listeners.java
│ │ │ ├── MissingDependencyException.java
│ │ │ ├── ModDefinition.java
│ │ │ ├── ModDefinitions.java
│ │ │ ├── ModDependencies.java
│ │ │ ├── ModDependency.java
│ │ │ ├── ModInstaller.java
│ │ │ ├── ModInstallerException.java
│ │ │ ├── ProgressListener.java
│ │ │ ├── ProgressListeners.java
│ │ │ ├── Resolution.java
│ │ │ ├── SourcesChangedListener.java
│ │ │ ├── SourcesChangedListeners.java
│ │ │ ├── Version.java
│ │ │ ├── VersionRequirement.java
│ │ │ ├── compatibility
│ │ │ ├── CompatibilityChecker.java
│ │ │ ├── CompatibilityState.java
│ │ │ ├── CompatibilityVersion.java
│ │ │ └── CompatibilityVersions.java
│ │ │ ├── repository
│ │ │ ├── DependencyResolution.java
│ │ │ ├── DownloadResponseExtractor.java
│ │ │ ├── Installation.java
│ │ │ ├── Installations.java
│ │ │ ├── RateLimitException.java
│ │ │ ├── Repository.java
│ │ │ ├── RepositoryException.java
│ │ │ ├── Source.java
│ │ │ ├── SourceDescription.java
│ │ │ ├── SourceException.java
│ │ │ ├── SourceFactory.java
│ │ │ ├── Sources.java
│ │ │ └── source
│ │ │ │ ├── AbstractSourceFactory.java
│ │ │ │ ├── DirectSourceFactory.java
│ │ │ │ ├── FileSourceFactory.java
│ │ │ │ ├── GithubAsset.java
│ │ │ │ ├── GithubAuthor.java
│ │ │ │ ├── GithubRelease.java
│ │ │ │ └── GithubSourceFactory.java
│ │ │ ├── rest
│ │ │ ├── HostUnreachableException.java
│ │ │ ├── RateLimitException.java
│ │ │ ├── RestClient.java
│ │ │ └── RestClientException.java
│ │ │ ├── ui
│ │ │ ├── BindingsFactory.java
│ │ │ ├── ChangeLogViewer.fxml
│ │ │ ├── ChangeLogViewerController.java
│ │ │ ├── ControllerFactory.java
│ │ │ ├── InstallerMainPanel.fxml
│ │ │ ├── InstallerMainPanelController.java
│ │ │ ├── ModDetailsPanel.fxml
│ │ │ ├── ModDetailsPanelController.java
│ │ │ ├── ModInstallerEvent.java
│ │ │ ├── ModInstallerMain.java
│ │ │ ├── ModInstallerUI.java
│ │ │ ├── ProgressDialog.fxml
│ │ │ ├── ProgressDialogController.java
│ │ │ ├── RefreshableObjectProperty.java
│ │ │ └── WindowBecameVisibleHandler.java
│ │ │ ├── update
│ │ │ ├── UpdateChecker.java
│ │ │ └── UpdateState.java
│ │ │ └── utils
│ │ │ ├── JsonUtils.java
│ │ │ ├── OsUtils.java
│ │ │ └── StringUtils.java
│ └── resources
│ │ ├── add_circle_outline_black_24x24.png
│ │ ├── arrow_upward_black_18x18.png
│ │ ├── baseline_access_time_red_18x18.png
│ │ ├── baseline_access_time_red_24x24.png
│ │ ├── baseline_filter_list_black_24x24.png
│ │ ├── default-compatibility-state.json
│ │ ├── default-sources.json
│ │ ├── folder_open_black_24.png
│ │ ├── global.css
│ │ ├── icon.png
│ │ ├── lock_grey_18x18.png
│ │ ├── lock_grey_24x24.png
│ │ ├── new_releases_grey_18x18.png
│ │ └── refresh_black_24x24.png
└── test
│ ├── java
│ └── me
│ │ └── wulfmarius
│ │ └── modinstaller
│ │ ├── DependencyResolverTest.java
│ │ └── VersionTest.java
│ └── resources
│ ├── mod-a.json
│ ├── mod-b.json
│ ├── mod-c.json
│ ├── mod-d.json
│ └── mod-e.json
└── tld-versions.json
/.gitignore:
--------------------------------------------------------------------------------
1 | mod-installer/
2 | mods/
3 | target/
4 | .classpath
5 | .project
6 | .settings
7 | dependency-reduced-pom.xml
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 WulfMarius
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Mod-Installer
2 |
3 |
4 |
--------------------------------------------------------------------------------
/descriptions/alextheregent-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AlexTheRegent Collection",
3 | "description": "Mods of AlexTheRegent",
4 | "url": "https://github.com/AlexTheRegent",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/AlexTheRegent/tld-GameTweaks"
8 | ]
9 | }
--------------------------------------------------------------------------------
/descriptions/default-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Default Collection",
3 | "description": "Default Mod Collection",
4 | "url": "https://github.com/WulfMarius/Mod-Installer/tree/master/descriptions",
5 | "releases": [],
6 | "definitions": [
7 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/alextheregent-mod-installer-description.json",
8 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/findarkside-mod-installer-description.json",
9 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/lucasio6-mod-installer-description.json",
10 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/mikeypdog-mod-installer-description.json",
11 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/nbakulev-mod-installer-description.json",
12 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/shieldheart-mod-installer-description.json",
13 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/uberfox-mod-installer-description.json",
14 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/wulfmarius-mod-installer-description.json",
15 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/xpazeman-mod-installer-description.json",
16 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/zeobviouslyfakeacc-mod-installer-description.json"
17 | ]
18 | }
--------------------------------------------------------------------------------
/descriptions/findarkside-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FINDarkside Collection",
3 | "description": "Mods of FINDarkside",
4 | "url": "https://github.com/FINDarkside",
5 | "author": "FINDarkside",
6 | "releases": [
7 | {
8 | "name": "Developer-Console",
9 | "version": "1.1",
10 | "releaseDate": "2018-11-04",
11 | "description": "Enables the developer console, which can be opened by pressing F1\nType help for a list of commands and press TAB to autocomplete.",
12 | "url": "https://github.com/FINDarkside/TLD-Developer-Console/releases/tag/1.1",
13 | "changes": "- Scroll down when a command is submitted\n- Fixed save, currentSceneName (now called scene_name), and currentSceneIndex (now called scene_index\n- Added new pos command to get the current position\n- Added new tp command to teleport",
14 | "compatibleWith": "V1.41",
15 | "dependencies": [],
16 | "assets": [
17 | {
18 | "url": "https://github.com/FINDarkside/TLD-Developer-Console/releases/download/1.1/DeveloperConsole.dll"
19 | }
20 | ]
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/descriptions/lucasio6-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Lucasio6 Collection",
3 | "description": "Mods of Lucasio6",
4 | "url": "https://github.com/lucasio6",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/lucasio6/Flint",
8 | "https://github.com/lucasio6/Lint",
9 | "https://github.com/lucasio6/Wolfhat"
10 | ]
11 | }
--------------------------------------------------------------------------------
/descriptions/mikeypdog-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MikeyPdog Collection",
3 | "description": "Mods of MikeyPdog",
4 | "url": "https://github.com/MikeyPdog",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/MikeyPdog/TLD-Minimods"
8 | ]
9 | }
--------------------------------------------------------------------------------
/descriptions/nbakulev-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nbakulev Collection",
3 | "description": "Mods of nbakulev",
4 | "url": "https://github.com/nbakulev",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/nbakulev/Deerskincoat",
8 | "https://github.com/nbakulev/Woodenstatuettes"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/descriptions/shieldheart-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ShieldHeart Collection",
3 | "description": "Mods of ShieldHeart",
4 | "url": "http://www.moddb.com/members/shieldhearttld",
5 | "author": "ShieldHeart",
6 | "releases": [
7 | {
8 | "name": "RelentlessNight",
9 | "version": "v3.00",
10 | "releaseDate": "2018-03-25",
11 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.",
12 | "url": "http://www.moddb.com/mods/relentlessnight/downloads/relentlessnight-v3-00",
13 | "changes": "- (new) All mod gameplay features can now be customized or turned off individually through the custom game setup menu.\n- (new) Indoor environments will retain heat from fires much longer after the fire has burnt out. The hotter the fire, the longer it will keep the place warm. In enclosed buildings, fires will heat the whole house/space and better insulated environments will keep warm for longer.\n- (new) Temperature will now affect wildlife populations. Roaming wildlife will be less abundant and harder to find in extreme cold temperatures, but also more abundant in relatively warm temperatures.\n- (new) You can now adjust how long fuels will burn in a fire or how long lantern fuel will last while in use. All can be adjusted to provide up to 3 times their regular burn times.\n- (new) Fixed bug in vanilla game where loot inside of containers in custom games would generate incorrectly, causing things [like this](). The correct amount of loot will now generate for the associated difficulty chosen in custom mode.\n- (new) Fixed bug in vanilla game that prevented custom mode option \"Wake Up When Freezing\" from working. This feature is now functional and enabled in all Relentless Night games.\n- Days and nights get increasingly longer eventually ending in the tidal locking of the Earth and survival in perpetual darkness.\n- Temperatures become increasingly warmer/colder with duration of days/nights.\n- Indoor temperatures now have a slight correlation with outdoor temperature.\n- Blizzards and stronger winds become more frequent as days/nights become longer and weather becomes more extreme.\n- Roaming wildlife become increasingly scarce as outdoor conditions become more extreme and harsh.\n- Cabin fever affliction is disabled due to the eventuality of long nights and extreme outdoor temperatures.\n- Indoor environments can become slightly lit during nights depending on weather and available moonlight outdoors. During the brightest phases of the moon, crafting/breaking of items indoors can become available again.\n- Surveying is now possible at night, provided the weather is clear and enough light is present.\n- For every additional 1C below a feels-like temperature of -40C, the rate at which the player suffers freezing damage is increased. This effect can accumulate up to a maximum of 5x the original rate at a -100C feels-like temperature and below, do bring a jacket.\n- The mod scenario can be played in all sandbox difficulties and can be set up in its own sandbox page. The mod does not prevent playing regular sandbox or other vanilla modes. Relentless night saves are kept completely separate from other save data and cannot be played by regular sandbox mode and vice versa.\n- Note: Journal statistics and days survived are still tracked in regular 24-hour days, as if the player is still keeping track with a watch.\n",
14 | "dependencies": [],
15 | "assets": [
16 | {
17 | "url": "https://www.moddb.com/downloads/mirror/135609/114/216ad7337ebffbc465a517399d7515cd",
18 | "zipDirectory": "ModFiles",
19 | "type": "zip"
20 | }
21 | ]
22 | }, {
23 | "name": "RelentlessNight",
24 | "version": "v3.01",
25 | "releaseDate": "2018-06-19",
26 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.",
27 | "url": "https://www.moddb.com/mods/relentlessnight/downloads/relentless-night-v301-133",
28 | "changes": "- Updated all mod features to work with new Vigilant Flame update.\n- Coals can now be added to a fire without a waiting period (late-game balancing).\n- More fuel can be added to a fire even if the fire has reached its maximum duration, provided that the fuel can still increase the fire's current temperature. Although the duration of fire will not increase further, the heat bonus will still be applied. (late-game balancing)\n- The immediate area where an outdoor fire burned will also retain some heat a while longer after the fire's out, although of course the temperature bonus will drop and disappear much quicker compared to the heat from a fire inside an insulated building.\n- Fixed mod bug where failing to start a fire indoors could reset the residual heat bonus remaining inside back to zero in RN's heat retention feature.\n- Fixed mod bug where blizzard temperature drop didn't change temperature during blizzards in certain Relentless Night runs.\n- Fixed mod bug where the temperature of outdoors caves would sometimes get calculated incorrectly and were warmer than intended.\n- Fixed mod bug where custom mode option \"Day Length Multiplier\" would cause timing issues in RN runs if it was set above 1x. All values of this option can now be chosen for RN runs as well.\n- Should you need to correct something in the RN settings later into your game, you can now change specific setting values through new console commands. Loading the save you want to change and entering \"rn_help\" into the console will provide information on all RN commands needed to change each setting.",
29 | "dependencies": [],
30 | "assets": [
31 | {
32 | "url": "https://www.moddb.com/downloads/mirror/138754/108/2616759aa426e06e45dc22e3a1bdee7f",
33 | "zipDirectory": "ModFiles",
34 | "type": "zip"
35 | }
36 | ]
37 | }, {
38 | "name": "RelentlessNight",
39 | "version": "v3.02",
40 | "releaseDate": "2019-01-06",
41 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.",
42 | "url": "https://github.com/Shield-Heart/RelentlessNight",
43 | "changes": "Bugfix update for v3.01 to work with the TLD v1.41 Redux update",
44 | "dependencies": [
45 | {
46 | "name": "ModSettings",
47 | "version": "^v1.4"
48 | }
49 | ],
50 | "assets": [
51 | {
52 | "url": "https://github.com/Shield-Heart/RelentlessNight/releases/download/v3.02/RelentlessNight.dll"
53 | }
54 | ]
55 | }
56 | ]
57 | }
--------------------------------------------------------------------------------
/descriptions/uberfox-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "UberFoX Collection",
3 | "description": "Mods of UberFoX",
4 | "url": "http://ubersoft.org/",
5 | "author": "UberFoX",
6 | "releases": [
7 | {
8 | "name": "UModTld",
9 | "version": "1.7",
10 | "releaseDate": "2017-12-12",
11 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.",
12 | "url": "http://ubersoft.org/umodtld/",
13 | "changes": "Updated UModTld to fully support the new features of the December update so it can spawn mooses and heal broken ribs etc etc.\nAlso added a new command \"/custom\" that gives you the code for your current custom game just so you never lose it!.\n\nAdded various more things to the Config file such as jump settings, ability to show time of day on screen, hotkeys to spawn wildlife etc etc and all kinds of good stuff.",
14 | "dependencies": [],
15 | "assets": [
16 | {
17 | "url": "http://ubersoft.org/download/UModTld17.zip"
18 | }
19 | ]
20 | }, {
21 | "name": "UModTld",
22 | "version": "1.7a",
23 | "releaseDate": "2018-03-13",
24 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.",
25 | "url": "http://ubersoft.org/umodtld/",
26 | "changes": "Updated UModTld to newest Long Dark version and newest Mod Loader since it seems a lot of bullshit was changed.",
27 | "dependencies": [],
28 | "assets": [
29 | {
30 | "url": "http://ubersoft.org/download/UModTld17a.zip"
31 | }
32 | ]
33 | }, {
34 | "name": "UModTld",
35 | "version": "1.7b",
36 | "releaseDate": "2018-06-25",
37 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.",
38 | "url": "http://ubersoft.org/umodtld/",
39 | "changes": "Updated version probably has nothing new hard to remember but fixed a bug in newer versions of TLD",
40 | "dependencies": [],
41 | "assets": [
42 | {
43 | "url": "http://ubersoft.org/download/UModTld17b.zip"
44 | }
45 | ]
46 | }, {
47 | "name": "UModTld",
48 | "version": "1.8a",
49 | "releaseDate": "2018-10-08",
50 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.",
51 | "url": "http://ubersoft.org/umodtld/",
52 | "changes": "Just a quick update to UModTld this one allows other mod developers to add their own console commands to UModTld",
53 | "dependencies": [],
54 | "assets": [
55 | {
56 | "url": "http://ubersoft.org/download/UModTld18a.zip"
57 | }
58 | ]
59 | }, {
60 | "name": "UModTld",
61 | "version": "1.9",
62 | "releaseDate": "2018-12-21",
63 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.",
64 | "url": "http://ubersoft.org/umodtld/",
65 | "changes": "Updated UModTld to work on newest version now that I'm back",
66 | "dependencies": [],
67 | "assets": [
68 | {
69 | "url": "https://www.moddb.com/downloads/mirror/172406/115/c68a2d3c87d4393cff25becaec41eedb",
70 | "type": "zip"
71 | }
72 | ]
73 | }
74 | ]
75 | }
--------------------------------------------------------------------------------
/descriptions/wulfmarius-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Wulf Marius Collection",
3 | "description": "Mods of Wulf Marius",
4 | "url": "https://github.com/WulfMarius",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/WulfMarius/Ankle-Support",
8 | "https://github.com/WulfMarius/AssetLoader",
9 | "https://github.com/WulfMarius/Better-Fuel-Management",
10 | "https://github.com/WulfMarius/Better-Night-Sky",
11 | "https://github.com/WulfMarius/Better-Placing",
12 | "https://github.com/WulfMarius/Better-Stacking",
13 | "https://github.com/WulfMarius/Better-Water-Management",
14 | "https://github.com/WulfMarius/Binoculars",
15 | "https://github.com/WulfMarius/Clothing-Pack",
16 | "https://github.com/WulfMarius/Coordinates-Grabber",
17 | "https://github.com/WulfMarius/Disable-Cabin-Fever",
18 | "https://github.com/WulfMarius/Disable-Chromatic-Aberration",
19 | "https://github.com/WulfMarius/Durable-Whetstone",
20 | "https://github.com/WulfMarius/Food-Pack",
21 | "https://github.com/WulfMarius/Free-Look-In-Cars",
22 | "https://github.com/WulfMarius/Home-Improvement",
23 | "https://github.com/WulfMarius/Instrument-Pack",
24 | "https://github.com/WulfMarius/Lonely-Orca",
25 | "https://github.com/WulfMarius/Map-Maker-Tools",
26 | "https://github.com/WulfMarius/ModComponent",
27 | "https://github.com/WulfMarius/NAudio-Unity",
28 | "https://github.com/WulfMarius/Preselect-Struggle-Weapon",
29 | "https://github.com/WulfMarius/Remove-Campfire",
30 | "https://github.com/WulfMarius/Silent-Aurora",
31 | "https://github.com/WulfMarius/Show-Map-Location",
32 | "https://github.com/WulfMarius/Solstice",
33 | "https://github.com/WulfMarius/Sort-Fire-Starters",
34 | "https://github.com/WulfMarius/Toggle-HUD"
35 | ]
36 | }
--------------------------------------------------------------------------------
/descriptions/xpazeman-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Xpazeman Collection",
3 | "description": "Mods from xpazeman",
4 | "url": "https://github.com/Xpazeman",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/Xpazeman/Mod-Installer-Description"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/descriptions/zeobviouslyfakeacc-mod-installer-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zeobviouslyfakeacc Collection",
3 | "description": "Mods of zeobviouslyfakeacc",
4 | "url": "https://github.com/zeobviouslyfakeacc",
5 | "releases": [],
6 | "definitions": [
7 | "https://github.com/zeobviouslyfakeacc/Mod-Installer-Description"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/images/installation-directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/installation-directory.png
--------------------------------------------------------------------------------
/images/installer-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/installer-01.png
--------------------------------------------------------------------------------
/images/update-available.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/update-available.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | jar
6 |
7 | me.wulfmarius
8 | mod-installer
9 | 0.7.3
10 | Mod-Installer
11 | 2018
12 |
13 |
14 |
15 | MIT License
16 | http://www.opensource.org/licenses/mit-license.php
17 |
18 |
19 |
20 |
21 | UTF-8
22 | 1.8
23 |
24 |
25 |
26 |
27 |
28 |
29 | org.apache.maven.plugins
30 | maven-compiler-plugin
31 | 3.1
32 |
33 | ${project.build.compiler.version}
34 | ${project.build.compiler.version}
35 | ${project.build.sourceEncoding}
36 |
37 |
38 |
39 | org.apache.maven.plugins
40 | maven-shade-plugin
41 | 3.1.1
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.apache.maven.plugins
49 | maven-shade-plugin
50 |
51 |
52 |
53 |
54 | sandbox
55 | 8.0
56 | me.wulfmarius.modinstaller.ui.ModInstallerUI
57 | WulfMarius
58 | ${project.version}
59 |
60 |
61 |
62 |
63 |
64 |
65 | package
66 |
67 | shade
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | src/main/java
77 |
78 | **/*.fxml
79 |
80 |
81 |
82 | src/main/resources
83 |
84 | *
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | com.fasterxml.jackson.core
93 | jackson-databind
94 | 2.9.8
95 |
96 |
97 | org.springframework
98 | spring-web
99 | 5.0.5.RELEASE
100 |
101 |
102 | junit
103 | junit
104 | 4.12
105 | test
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/AbortException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class AbortException extends ModInstallerException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | public AbortException(String message) {
8 | super(message);
9 | }
10 |
11 | public AbortException(String message, Throwable cause) {
12 | super(message, cause);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/Asset.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class Asset {
4 |
5 | private String url;
6 | private String targetDirectory;
7 | private String zipDirectory;
8 | private String type;
9 |
10 | public static Asset withUrl(String url) {
11 | Asset result = new Asset();
12 | result.setUrl(url);
13 | return result;
14 | }
15 |
16 | public String getTargetDirectory() {
17 | return this.targetDirectory;
18 | }
19 |
20 | public String getType() {
21 | return this.type;
22 | }
23 |
24 | public String getUrl() {
25 | return this.url;
26 | }
27 |
28 | public String getZipDirectory() {
29 | return this.zipDirectory;
30 | }
31 |
32 | public void setTargetDirectory(String targetDirectory) {
33 | this.targetDirectory = targetDirectory;
34 | }
35 |
36 | public void setType(String type) {
37 | this.type = type;
38 | }
39 |
40 | public void setUrl(String url) {
41 | this.url = url;
42 | }
43 |
44 | public void setZipDirectory(String zipDirectory) {
45 | this.zipDirectory = zipDirectory;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/DependencyResolver.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.*;
4 | import java.util.stream.Collectors;
5 |
6 | import me.wulfmarius.modinstaller.repository.*;
7 |
8 | public class DependencyResolver {
9 |
10 | private final Repository repository;
11 | private final Installations installations;
12 |
13 | private final ModDefinitions installed = new ModDefinitions();
14 | private final Resolution resolution = new Resolution();
15 |
16 | public DependencyResolver(Repository repository, Installations installations) {
17 | super();
18 |
19 | this.repository = repository;
20 | this.installations = installations;
21 | }
22 |
23 | public Installations getInstallations() {
24 | return this.installations;
25 | }
26 |
27 | public Repository getRepository() {
28 | return this.repository;
29 | }
30 |
31 | public Resolution resolve(ModDefinition modDefinition) {
32 | this.initializeInstalled();
33 | this.resolveDependencies(modDefinition);
34 | this.consolidate();
35 |
36 | return this.resolution;
37 | }
38 |
39 | private void consolidate() {
40 | for (Iterator iterator = this.resolution.getInstall().iterator(); iterator.hasNext();) {
41 | ModDefinition eachInstall = iterator.next();
42 |
43 | if (this.resolution.getUninstall().contains(eachInstall)) {
44 | iterator.remove();
45 | this.resolution.getUninstall().remove(eachInstall);
46 | continue;
47 | }
48 |
49 | if (this.installations.contains(eachInstall)) {
50 | iterator.remove();
51 | }
52 | }
53 |
54 | this.resolution.getInstall().reverse();
55 | }
56 |
57 | private DependencyResolution findMatchingVersion(ModDependencies dependencies) {
58 | DependencyResolution result = new DependencyResolution();
59 | result.setRequested(dependencies);
60 |
61 | Map matchingDependencies = new LinkedHashMap();
62 |
63 | for (ModDependency eachDependency : dependencies) {
64 | matchingDependencies.put(eachDependency, this.repository.getMatching(eachDependency));
65 | }
66 |
67 | result.setAvailable(matchingDependencies.values().stream().flatMap(ModDefinitions::stream).collect(Collectors.toSet()));
68 |
69 | ModDefinitions candidates = matchingDependencies.values().stream().reduce(null, ModDefinitions::intersect);
70 | candidates.getMin(ModDefinition::latest).ifPresent(result::setBestMatch);
71 |
72 | return result;
73 | }
74 |
75 | private void initializeInstalled() {
76 | this.installations.stream()
77 | .map(installation -> this.repository.getModDefinition(installation.getName(), installation.getVersion()))
78 | .filter(Optional::isPresent)
79 | .map(Optional::get)
80 | .forEach(this.installed::addModDefinition);
81 | }
82 |
83 | private void install(ModDefinition bestMatch) {
84 | this.resolution.addInstall(bestMatch);
85 | this.installed.addModDefinition(bestMatch);
86 | }
87 |
88 | private void resolveDependencies(ModDefinition modDefinition) {
89 | ModDefinition next = modDefinition;
90 |
91 | while (next != null) {
92 | this.uninstall(next);
93 | this.install(next);
94 |
95 | DependencyResolution dependencyResolution = this.installed.getAllDependencies()
96 | .values()
97 | .stream()
98 | .filter(dependencies -> !this.installed.satisfiesAll(dependencies))
99 | .map(this::findMatchingVersion)
100 | .findFirst()
101 | .orElseGet(DependencyResolution::empty);
102 |
103 | if (!dependencyResolution.isAvailable()) {
104 | this.resolution.setMissingDependencies(dependencyResolution.getRequested());
105 | } else if (!dependencyResolution.isResolved()) {
106 | this.resolution.setUnresolvableDependencies(dependencyResolution.getRequested());
107 | }
108 |
109 | next = dependencyResolution.getBestMatch();
110 | }
111 | }
112 |
113 | private void uninstall(ModDefinition modDefinition) {
114 | ModDefinitions installedVersions = this.installed.getModDefinitions(modDefinition.getName());
115 | for (ModDefinition eachInstalledVersion : installedVersions) {
116 | this.installed.remove(eachInstalledVersion);
117 | }
118 |
119 | this.resolution.addUninstalls(this.installations.getInstallations(modDefinition.getName()));
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/InstallationsChangedListener.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | @FunctionalInterface
4 | public interface InstallationsChangedListener {
5 |
6 | void changed();
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/InstallationsChangedListeners.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class InstallationsChangedListeners extends Listeners {
4 |
5 | public void changed() {
6 | this.fire(listener -> listener.changed());
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/Listeners.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.*;
4 | import java.util.function.Consumer;
5 |
6 | public class Listeners {
7 |
8 | private final Set listeners = new HashSet<>();
9 |
10 | public void addListener(T listener) {
11 | this.listeners.add(listener);
12 | }
13 |
14 | public void fire(Consumer super T> action) {
15 | this.listeners.forEach(action);
16 | }
17 |
18 | public void removeListener(T listener) {
19 | this.listeners.remove(listener);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/MissingDependencyException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class MissingDependencyException extends ModInstallerException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | private final ModDependencies dependencies;
8 |
9 | public MissingDependencyException(String message, ModDependencies dependencies) {
10 | super(message);
11 | this.dependencies = dependencies;
12 | }
13 |
14 | public ModDependencies getDependencies() {
15 | return this.dependencies;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ModDefinition.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.*;
4 | import java.util.stream.Stream;
5 |
6 | import com.fasterxml.jackson.annotation.JsonIgnore;
7 |
8 | public class ModDefinition {
9 |
10 | private String sourceDefinition;
11 |
12 | private String name;
13 | private String url;
14 | private String author;
15 | private String version;
16 | private String description;
17 | private String changes;
18 | private Date releaseDate;
19 | private Date lastUpdated;
20 | private Asset[] assets;
21 | private ModDependency[] dependencies;
22 | private String compatibleWith;
23 |
24 | @JsonIgnore
25 | private transient Version parsedVersion;
26 |
27 | @JsonIgnore
28 | private transient Version parsedCompatibleWith;
29 |
30 | public static int latest(ModDefinition m1, ModDefinition m2) {
31 | return -Version.compare(m1.getParsedVersion(), m2.getParsedVersion());
32 | }
33 |
34 | public boolean dependsOn(String modName) {
35 | return this.getDependenciesStream().anyMatch(dependency -> dependency.getName().equals(modName));
36 | }
37 |
38 | @Override
39 | public boolean equals(Object obj) {
40 | if (this == obj) {
41 | return true;
42 | }
43 |
44 | if (!(obj instanceof ModDefinition)) {
45 | return false;
46 | }
47 |
48 | ModDefinition other = (ModDefinition) obj;
49 | if (!this.name.equals(other.name)) {
50 | return false;
51 | }
52 |
53 | if (this.getParsedVersion().compareTo(other.getParsedVersion()) != 0) {
54 | return false;
55 | }
56 |
57 | return true;
58 | }
59 |
60 | public Asset[] getAssets() {
61 | return this.assets;
62 | }
63 |
64 | public String getAuthor() {
65 | return this.author;
66 | }
67 |
68 | public String getChanges() {
69 | return this.changes;
70 | }
71 |
72 | public String getCompatibleWith() {
73 | return this.compatibleWith;
74 | }
75 |
76 | public ModDependency[] getDependencies() {
77 | return this.dependencies;
78 | }
79 |
80 | public Stream getDependenciesStream() {
81 | if (this.dependencies == null) {
82 | return Stream.empty();
83 | }
84 |
85 | return Arrays.stream(this.dependencies);
86 | }
87 |
88 | public String getDescription() {
89 | return this.description;
90 | }
91 |
92 | public String getDisplayName() {
93 | return this.name + " " + this.version;
94 | }
95 |
96 | public Date getLastUpdated() {
97 | return this.lastUpdated;
98 | }
99 |
100 | public String getName() {
101 | return this.name;
102 | }
103 |
104 | public Version getParsedCompatibleWith() {
105 | if (this.parsedCompatibleWith == null && this.compatibleWith != null) {
106 | this.parsedCompatibleWith = Version.parse(this.compatibleWith);
107 | }
108 |
109 | return this.parsedCompatibleWith;
110 | }
111 |
112 | public Date getReleaseDate() {
113 | return this.releaseDate;
114 | }
115 |
116 | public String getSourceDefinition() {
117 | return this.sourceDefinition;
118 | }
119 |
120 | public String getUrl() {
121 | return this.url;
122 | }
123 |
124 | public String getVersion() {
125 | return this.version;
126 | }
127 |
128 | @Override
129 | public int hashCode() {
130 | final int prime = 31;
131 | int result = 1;
132 | result = prime * result + (this.name == null ? 0 : this.name.hashCode());
133 | result = prime * result + (this.version == null ? 0 : this.version.hashCode());
134 | return result;
135 | }
136 |
137 | public boolean satisfies(ModDependency dependency) {
138 | if (!this.name.equals(dependency.getName())) {
139 | return false;
140 | }
141 |
142 | return dependency.isSatisfiedBy(this.getParsedVersion());
143 | }
144 |
145 | public void setAssets(Asset[] assets) {
146 | this.assets = assets;
147 | }
148 |
149 | public void setAuthor(String author) {
150 | this.author = author;
151 | }
152 |
153 | public void setChanges(String changes) {
154 | this.changes = changes;
155 | }
156 |
157 | public void setCompatibleWith(String compatibleWith) {
158 | this.compatibleWith = compatibleWith;
159 | }
160 |
161 | public void setDependencies(ModDependency[] dependencies) {
162 | this.dependencies = dependencies;
163 | }
164 |
165 | public void setDescription(String description) {
166 | this.description = description;
167 | }
168 |
169 | public void setLastUpdated(Date lastUpdated) {
170 | this.lastUpdated = lastUpdated;
171 | }
172 |
173 | public void setName(String name) {
174 | this.name = name;
175 | }
176 |
177 | public void setReleaseDate(Date releaseDate) {
178 | this.releaseDate = releaseDate;
179 | }
180 |
181 | public void setSourceDefinition(String sourceDefinition) {
182 | this.sourceDefinition = sourceDefinition;
183 | }
184 |
185 | public void setUrl(String url) {
186 | this.url = url;
187 | }
188 |
189 | public void setVersion(String version) {
190 | this.version = version;
191 | }
192 |
193 | @Override
194 | public String toString() {
195 | return "(Definition " + this.name + ", " + this.version + ")";
196 | }
197 |
198 | private Version getParsedVersion() {
199 | if (this.parsedVersion == null) {
200 | this.parsedVersion = Version.parse(this.version);
201 | }
202 |
203 | return this.parsedVersion;
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ModDefinitions.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.*;
4 | import java.util.stream.*;
5 |
6 | import com.fasterxml.jackson.annotation.*;
7 |
8 | public class ModDefinitions implements Iterable {
9 |
10 | @JsonValue
11 | private List modDefinitions = new ArrayList<>();
12 |
13 | @JsonCreator
14 | public static ModDefinitions create(ModDefinition... modDefinition) {
15 | ModDefinitions result = new ModDefinitions();
16 |
17 | if (modDefinition != null) {
18 | for (ModDefinition eachModDefinition : modDefinition) {
19 | result.addModDefinition(eachModDefinition);
20 | }
21 | }
22 |
23 | return result;
24 | }
25 |
26 | public static ModDefinitions intersect(ModDefinitions definitions1, ModDefinitions definitions2) {
27 | if (definitions1 == null) {
28 | return definitions2;
29 | }
30 |
31 | if (definitions2 == null) {
32 | return definitions1;
33 | }
34 |
35 | ModDefinitions result = new ModDefinitions();
36 |
37 | for (ModDefinition eachDefinition : definitions1) {
38 | if (definitions2.contains(eachDefinition)) {
39 | result.addModDefinition(eachDefinition);
40 | }
41 | }
42 |
43 | return result;
44 | }
45 |
46 | public static ModDefinitions merge(ModDefinitions definitions1, ModDefinitions definitions2) {
47 | ModDefinitions result = new ModDefinitions();
48 |
49 | result.addModDefinitions(definitions1);
50 | result.addModDefinitions(definitions2);
51 |
52 | return result;
53 | }
54 |
55 | public static Collector toModDefinitions() {
56 | return Collectors.reducing(new ModDefinitions(), ModDefinitions::create, ModDefinitions::merge);
57 | }
58 |
59 | public void addModDefinition(ModDefinition definition) {
60 | if (!this.modDefinitions.contains(definition)) {
61 | this.modDefinitions.add(definition);
62 | }
63 | }
64 |
65 | public void addModDefinitions(Iterable definitions) {
66 | definitions.forEach(this::addModDefinition);
67 | }
68 |
69 | public boolean contains(ModDefinition definition) {
70 | return this.modDefinitions.contains(definition);
71 | }
72 |
73 | public Map getAllDependencies() {
74 | return this.modDefinitions.stream().flatMap(ModDefinition::getDependenciesStream).collect(
75 | Collectors.toMap(ModDependency::getName, ModDependencies::create, ModDependencies::merge));
76 | }
77 |
78 | public ModDefinitions getMatchingDefinitions(ModDependency dependency) {
79 | return this.modDefinitions.stream().filter(definition -> definition.satisfies(dependency)).collect(toModDefinitions());
80 | }
81 |
82 | public Optional getMin(Comparator super ModDefinition> comparator) {
83 | return this.modDefinitions.stream().min(comparator);
84 | }
85 |
86 | public Optional getModDefinition(String name, String version) {
87 | return this.modDefinitions.stream()
88 | .filter(definition -> definition.getName().equals(name) && definition.getVersion().equals(version))
89 | .findFirst();
90 | }
91 |
92 | public ModDefinitions getModDefinitions(String name) {
93 | return this.modDefinitions.stream().filter(modDefinition -> modDefinition.getName().equals(name)).collect(toModDefinitions());
94 | }
95 |
96 | public int getSize() {
97 | if (this.modDefinitions.isEmpty()) {
98 | return 0;
99 | }
100 |
101 | return this.modDefinitions.size();
102 | }
103 |
104 | public boolean isEmpty() {
105 | return this.modDefinitions == null || this.modDefinitions.isEmpty();
106 | }
107 |
108 | @Override
109 | public Iterator iterator() {
110 | return this.modDefinitions.iterator();
111 | }
112 |
113 | public void remove(ModDefinition modDefinition) {
114 | this.modDefinitions.remove(modDefinition);
115 | }
116 |
117 | public void remove(String name) {
118 | if (name == null) {
119 | return;
120 | }
121 |
122 | this.modDefinitions.removeIf(modDefinition -> name.equals(modDefinition.getName()));
123 | }
124 |
125 | public void reverse() {
126 | Collections.reverse(this.modDefinitions);
127 | }
128 |
129 | public boolean satisfies(ModDependency modDependency) {
130 | return this.modDefinitions.stream().anyMatch(modDefinition -> modDefinition.satisfies(modDependency));
131 | }
132 |
133 | public boolean satisfiesAll(ModDependencies modDependencies) {
134 | return modDependencies.stream().allMatch(this::satisfies);
135 | }
136 |
137 | public Stream stream() {
138 | return this.modDefinitions.stream();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ModDependencies.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.*;
4 | import java.util.stream.Stream;
5 |
6 | public class ModDependencies implements Iterable {
7 |
8 | private Set dependencies = new LinkedHashSet<>();
9 |
10 | public static ModDependencies create(ModDependency dependency) {
11 | ModDependencies result = new ModDependencies();
12 | result.dependencies.add(dependency);
13 | return result;
14 | }
15 |
16 | public static ModDependencies merge(ModDependencies dependencies1, ModDependencies dependencies2) {
17 | ModDependencies result = new ModDependencies();
18 | result.addAll(dependencies1);
19 | result.addAll(dependencies2);
20 | return result;
21 | }
22 |
23 | public void add(ModDependency modDependency) {
24 | this.dependencies.add(modDependency);
25 | }
26 |
27 | public void addAll(Iterable additionalDependencies) {
28 | additionalDependencies.forEach(this::add);
29 | }
30 |
31 | public boolean contains(ModDependency modDependency) {
32 | return this.dependencies.contains(modDependency);
33 | }
34 |
35 | public int getSize() {
36 | if (this.isEmpty()) {
37 | return 0;
38 | }
39 |
40 | return this.dependencies.size();
41 | }
42 |
43 | public boolean isEmpty() {
44 | return this.dependencies == null || this.dependencies.isEmpty();
45 | }
46 |
47 | @Override
48 | public Iterator iterator() {
49 | if (this.dependencies == null) {
50 | return Collections.emptyIterator();
51 | }
52 |
53 | return this.dependencies.iterator();
54 | }
55 |
56 | public Stream stream() {
57 | if (this.dependencies == null) {
58 | return Stream.empty();
59 | }
60 |
61 | return this.dependencies.stream();
62 | }
63 |
64 | @Override
65 | public String toString() {
66 | return this.dependencies.toString();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ModDependency.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 |
5 | public class ModDependency {
6 |
7 | private String name;
8 | private String version;
9 |
10 | @JsonIgnore
11 | private transient VersionRequirement versionRequirement;
12 |
13 | @Override
14 | public boolean equals(Object obj) {
15 | if (this == obj) {
16 | return true;
17 | }
18 |
19 | if (!(obj instanceof ModDependency)) {
20 | return false;
21 | }
22 |
23 | ModDependency other = (ModDependency) obj;
24 |
25 | if (!this.name.equals(other.name)) {
26 | return false;
27 | }
28 |
29 | if (!this.version.equals(other.version)) {
30 | return false;
31 | }
32 |
33 | return true;
34 | }
35 |
36 | public String getDisplayName() {
37 | return this.name + " " + this.version;
38 | }
39 |
40 | public String getName() {
41 | return this.name;
42 | }
43 |
44 | public String getVersion() {
45 | return this.version;
46 | }
47 |
48 | public VersionRequirement getVersionRequirement() {
49 | if (this.versionRequirement == null) {
50 | this.versionRequirement = VersionRequirement.parse(this.version);
51 | }
52 |
53 | return this.versionRequirement;
54 | }
55 |
56 | @Override
57 | public int hashCode() {
58 | final int prime = 31;
59 | int result = 1;
60 | result = prime * result + (this.name == null ? 0 : this.name.hashCode());
61 | result = prime * result + (this.version == null ? 0 : this.version.hashCode());
62 | return result;
63 | }
64 |
65 | public boolean isSatisfiedBy(Version actualVersion) {
66 | return this.getVersionRequirement().isSatisfiedBy(actualVersion);
67 | }
68 |
69 | public void setName(String name) {
70 | this.name = name;
71 | }
72 |
73 | public void setVersion(String version) {
74 | this.version = version;
75 | }
76 |
77 | @Override
78 | public String toString() {
79 | return "(Dependency: " + this.name + ", " + this.version + ")";
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ModInstallerException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class ModInstallerException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | public ModInstallerException(String message) {
8 | super(message);
9 | }
10 |
11 | public ModInstallerException(String message, Throwable cause) {
12 | super(message, cause);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ProgressListener.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public interface ProgressListener {
4 |
5 | void finished(String message);
6 |
7 | void started(String name);
8 |
9 | void stepDetail(String detail);
10 |
11 | void stepError(String error);
12 |
13 | void stepProgress(int completed, int total);
14 |
15 | void stepStarted(String step, StepType stepType);
16 |
17 | enum StepType {
18 | DOWNLOAD, INSTALL, UNINSTALL, REFRESH, ADD, INITIALIZE;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ProgressListeners.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import me.wulfmarius.modinstaller.ProgressListener.StepType;
4 |
5 | public class ProgressListeners extends Listeners {
6 |
7 | public void detail(String detail) {
8 | this.fire(listener -> listener.stepDetail(detail));
9 | }
10 |
11 | public void error(String error) {
12 | this.fire(listener -> listener.stepError(error));
13 | }
14 |
15 | public void finished() {
16 | this.finished(null);
17 | }
18 |
19 | public void finished(String message) {
20 | this.fire(listener -> listener.finished(message));
21 | }
22 |
23 | public void started(String name) {
24 | this.fire(listener -> listener.started(name));
25 | }
26 |
27 | public void stepProgress(int completed, int total) {
28 | this.fire(listener -> listener.stepProgress(completed, total));
29 | }
30 |
31 | public void stepStarted(String step, StepType stepType) {
32 | this.fire(listener -> listener.stepStarted(step, stepType));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/Resolution.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import me.wulfmarius.modinstaller.repository.*;
4 |
5 | public class Resolution {
6 |
7 | private ModDefinitions install = new ModDefinitions();
8 | private Installations uninstall = new Installations();
9 |
10 | private ModDependencies missingDependencies;
11 | private ModDependencies unresolvableDependencies;
12 |
13 | public void addInstall(ModDefinition modDefinition) {
14 | this.install.addModDefinition(modDefinition);
15 | }
16 |
17 | public void addUninstalls(Iterable uninstalls) {
18 | this.uninstall.addInstallations(uninstalls);
19 | }
20 |
21 | public boolean containsUninstall(Installation installation) {
22 | return this.uninstall.contains(installation);
23 | }
24 |
25 | public ModDefinitions getInstall() {
26 | return this.install;
27 | }
28 |
29 | public ModDependencies getMissingDependencies() {
30 | return this.missingDependencies;
31 | }
32 |
33 | public Installations getUninstall() {
34 | return this.uninstall;
35 | }
36 |
37 | public ModDependencies getUnresolvableDependencies() {
38 | return this.unresolvableDependencies;
39 | }
40 |
41 | public boolean hasMissingDependencies() {
42 | return this.missingDependencies != null && !this.missingDependencies.isEmpty();
43 | }
44 |
45 | public boolean hasUnresolvableDependencies() {
46 | return this.unresolvableDependencies != null && !this.unresolvableDependencies.isEmpty();
47 | }
48 |
49 | public boolean isEmpty() {
50 | return this.install.isEmpty();
51 | }
52 |
53 | public boolean isErroneous() {
54 | return this.hasMissingDependencies() || this.hasUnresolvableDependencies();
55 | }
56 |
57 | public void setInstall(ModDefinitions install) {
58 | this.install = install;
59 | }
60 |
61 | public void setMissingDependencies(ModDependencies missingDependencies) {
62 | this.missingDependencies = missingDependencies;
63 | }
64 |
65 | public void setUninstall(Installations uninstall) {
66 | this.uninstall = uninstall;
67 | }
68 |
69 | public void setUnresolvableDependencies(ModDependencies unresolvableDependencies) {
70 | this.unresolvableDependencies = unresolvableDependencies;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/SourcesChangedListener.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | @FunctionalInterface
4 | public interface SourcesChangedListener {
5 |
6 | void changed();
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/SourcesChangedListeners.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | public class SourcesChangedListeners extends Listeners {
4 |
5 | public void changed() {
6 | this.fire(listener -> listener.changed());
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/Version.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import static me.wulfmarius.modinstaller.utils.StringUtils.trimToEmpty;
4 |
5 | import java.util.Comparator;
6 | import java.util.regex.*;
7 |
8 | import org.springframework.util.StringUtils;
9 |
10 | public class Version implements Comparable {
11 |
12 | public static final String VERSION_UNKNOWN = "UNKNOWN";
13 |
14 | // an extension to the semver.org pattern to accomodate UModTld
15 | private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?(\\w+)?(?:-(\\w+))?");
16 |
17 | public static final Comparator COMPARATOR = Comparator.comparingInt(Version::getMajor)
18 | .thenComparingInt(Version::getMinor)
19 | .thenComparingInt(Version::getPatch)
20 | .thenComparing(Version::getSpecial, Version::compareSpecial)
21 | .thenComparing(Version::getPrelease, Version::comparePrerelease);
22 |
23 | private int major;
24 | private int minor;
25 | private int patch;
26 | private String prelease;
27 | private String special;
28 |
29 | public static int compare(Version v1, Version v2) {
30 | return COMPARATOR.compare(v1, v2);
31 | }
32 |
33 | public static Version parse(String version) {
34 | if (version.startsWith("v") || version.startsWith("V")) {
35 | return parse(version.substring(1));
36 | }
37 |
38 | Matcher matcher = VERSION_PATTERN.matcher(version);
39 | if (matcher.matches()) {
40 | Version result = new Version();
41 | result.major = Integer.parseInt(matcher.group(1));
42 |
43 | if (!StringUtils.isEmpty(matcher.group(2))) {
44 | result.minor = Integer.parseInt(matcher.group(2));
45 | }
46 |
47 | if (!StringUtils.isEmpty(matcher.group(3))) {
48 | result.patch = Integer.parseInt(matcher.group(3));
49 | }
50 |
51 | result.special = trimToEmpty(matcher.group(4));
52 | result.prelease = trimToEmpty(matcher.group(5));
53 |
54 | return result;
55 | }
56 |
57 | throw new IllegalArgumentException("Unsupported version format: " + version);
58 | }
59 |
60 | private static int comparePrerelease(String prerelease1, String prerelease2) {
61 | if (prerelease1.equals(prerelease2)) {
62 | return 0;
63 | }
64 |
65 | if (prerelease1.isEmpty()) {
66 | return 1;
67 | }
68 |
69 | if (prerelease2.isEmpty()) {
70 | return -1;
71 | }
72 |
73 | return prerelease1.compareTo(prerelease2);
74 | }
75 |
76 | private static int compareSpecial(String special1, String special2) {
77 | if (special1.equals(special2)) {
78 | return 0;
79 | }
80 |
81 | if (special1.isEmpty()) {
82 | return -1;
83 | }
84 |
85 | if (special2.isEmpty()) {
86 | return 1;
87 | }
88 |
89 | return special1.compareTo(special2);
90 | }
91 |
92 | @Override
93 | public int compareTo(Version other) {
94 | return compare(this, other);
95 | }
96 |
97 | @Override
98 | public boolean equals(Object obj) {
99 | if (this == obj) {
100 | return true;
101 | }
102 |
103 | if (!(obj instanceof Version)) {
104 | return false;
105 | }
106 |
107 | Version other = (Version) obj;
108 | return compare(this, other) == 0;
109 | }
110 |
111 | public int getMajor() {
112 | return this.major;
113 | }
114 |
115 | public int getMinor() {
116 | return this.minor;
117 | }
118 |
119 | public int getPatch() {
120 | return this.patch;
121 | }
122 |
123 | public String getPrelease() {
124 | return this.prelease;
125 | }
126 |
127 | public String getSpecial() {
128 | return this.special;
129 | }
130 |
131 | @Override
132 | public int hashCode() {
133 | final int prime = 31;
134 | int result = 1;
135 | result = prime * result + this.major;
136 | result = prime * result + this.minor;
137 | result = prime * result + this.patch;
138 | result = prime * result + (this.prelease == null ? 0 : this.prelease.hashCode());
139 | result = prime * result + (this.special == null ? 0 : this.special.hashCode());
140 | return result;
141 | }
142 |
143 | public boolean hasSameMajor(Version other) {
144 | return this.major == other.major;
145 | }
146 |
147 | public boolean hasSameMinor(Version other) {
148 | return this.minor == other.minor;
149 | }
150 |
151 | public boolean hasSamePatch(Version other) {
152 | return this.patch == other.patch;
153 | }
154 |
155 | public boolean hasSamePrelease(Version other) {
156 | if (this.prelease == null) {
157 | return other.prelease == null;
158 | }
159 |
160 | return this.prelease.equals(other.prelease);
161 | }
162 |
163 | public Version nextMajor() {
164 | Version result = new Version();
165 |
166 | result.major = this.major + 1;
167 | result.minor = 0;
168 | result.patch = 0;
169 | result.prelease = "";
170 | result.special = "";
171 |
172 | return result;
173 | }
174 |
175 | public void setMajor(int major) {
176 | this.major = major;
177 | }
178 |
179 | public void setMinor(int minor) {
180 | this.minor = minor;
181 | }
182 |
183 | public void setPatch(int patch) {
184 | this.patch = patch;
185 | }
186 |
187 | public void setPrelease(String prelease) {
188 | this.prelease = prelease;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/VersionRequirement.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import java.util.function.*;
4 |
5 | public class VersionRequirement {
6 |
7 | private Predicate predicate;
8 |
9 | public static VersionRequirement parse(String version) {
10 | VersionRequirement result = new VersionRequirement();
11 |
12 | if (version.startsWith("^")) {
13 | Version parsedVersion = Version.parse(version.substring(1));
14 | result.predicate = VersionComparison.atLeast(parsedVersion).and(VersionComparison.below(parsedVersion.nextMajor()));
15 | } else {
16 | Version parsedVersion = Version.parse(version.substring(0));
17 | result.predicate = VersionComparison.equals(parsedVersion);
18 | }
19 |
20 | return result;
21 | }
22 |
23 | public boolean isSatisfiedBy(Version version) {
24 | return this.predicate.test(version);
25 | }
26 |
27 | public static class VersionComparison implements Predicate {
28 |
29 | private final Version expectedVersion;
30 | private final BiFunction test;
31 |
32 | private VersionComparison(Version expectedVersion, BiFunction test) {
33 | super();
34 | this.expectedVersion = expectedVersion;
35 | this.test = test;
36 | }
37 |
38 | public static VersionComparison below(Version expectedVersion) {
39 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) > 0);
40 | }
41 |
42 | public static VersionComparison equals(Version expectedVersion) {
43 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) == 0);
44 | }
45 |
46 | public static VersionComparison atLeast(Version expectedVersion) {
47 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) <= 0);
48 | }
49 |
50 | @Override
51 | public boolean test(Version version) {
52 | return this.test.apply(this.expectedVersion, version);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityChecker.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.compatibility;
2 |
3 | import java.io.*;
4 | import java.nio.file.*;
5 | import java.util.*;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | import org.springframework.http.ResponseEntity;
9 |
10 | import me.wulfmarius.modinstaller.*;
11 | import me.wulfmarius.modinstaller.rest.RestClient;
12 | import me.wulfmarius.modinstaller.utils.JsonUtils;
13 |
14 | public class CompatibilityChecker {
15 |
16 | private static final String TLD_VERSIONS_URL = "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/tld-versions.json";
17 |
18 | private final Path basePath;
19 | private final RestClient restClient;
20 | private final CompatibilityState state;
21 |
22 | private final Map compatibilityCache = new ConcurrentHashMap<>();
23 |
24 | private String currentVersion = Version.VERSION_UNKNOWN;
25 | private Version parsedCurrentVersion;
26 | private CompatibilityVersion currentCompatibilityVersion;
27 |
28 | public CompatibilityChecker(Path basePath, RestClient restClient) {
29 | super();
30 |
31 | this.basePath = basePath;
32 | this.restClient = restClient;
33 | this.state = this.readState();
34 | }
35 |
36 | public Compatibility getCompatibility(ModDefinition modDefinition) {
37 | return this.compatibilityCache.computeIfAbsent(modDefinition, this::calculateCompatibility);
38 | }
39 |
40 | public String getCurrentVersion() {
41 | return this.currentVersion;
42 | }
43 |
44 | public CompatibilityState getState() {
45 | return this.state;
46 | }
47 |
48 | public void initialize() {
49 | this.readCurrentVersion();
50 | this.fetchTldVersions();
51 | }
52 |
53 | public void invalidate() {
54 | this.compatibilityCache.clear();
55 | }
56 |
57 | private Compatibility calculateCompatibility(ModDefinition modDefinition) {
58 | if (this.currentCompatibilityVersion == null) {
59 | return Compatibility.UNKNOWN;
60 | }
61 |
62 | CompatibilityVersion modCompatibilityVersion = Optional.ofNullable(modDefinition.getParsedCompatibleWith())
63 | .map(this.state.getCompatibilityVersions()::floor)
64 | .orElseGet(() -> this.state.getCompatibilityVersions().floor(modDefinition.getReleaseDate()));
65 |
66 | if (this.currentCompatibilityVersion.equals(modCompatibilityVersion)) {
67 | return Compatibility.OK;
68 | }
69 |
70 | return Compatibility.OLD;
71 | }
72 |
73 | private void fetchTldVersions() {
74 | try {
75 | ResponseEntity response = this.restClient.fetch(TLD_VERSIONS_URL, this.state.getEtag());
76 | if (response.getStatusCode().is2xxSuccessful()) {
77 | this.state.setCompatibilityVersions(this.restClient.deserialize(response, CompatibilityVersions.class, null));
78 | this.state.setEtag(response.getHeaders().getETag());
79 | this.currentCompatibilityVersion = this.state.getCompatibilityVersions().floor(this.parsedCurrentVersion);
80 | }
81 |
82 | this.state.setChecked(new Date());
83 | this.writeState();
84 | } catch (AbortException e) {
85 | // ignore
86 | }
87 | }
88 |
89 | private Path getStatePath() {
90 | return this.basePath.resolve("compatibility-state.json");
91 | }
92 |
93 | private Optional getVersionFilePath() {
94 | Optional optional = Optional.of(this.basePath.resolveSibling("tld_Data/StreamingAssets/version.txt"))
95 | .filter(Files::exists);
96 | if (optional.isPresent()) {
97 | return optional;
98 | }
99 |
100 | optional = Optional.of(this.basePath.resolveSibling("tld.app/Contents/Resources/Data/StreamingAssets/version.txt"))
101 | .filter(Files::exists);
102 | return optional;
103 | }
104 |
105 | private void readCurrentVersion() {
106 | Path path = this.getVersionFilePath().orElseThrow(() -> new ModInstallerException("Could not find TLD version file."));
107 |
108 | try {
109 | List lines = Files.readAllLines(path);
110 | if (lines.isEmpty()) {
111 | throw new ModInstallerException("TLD version file was empty.");
112 | }
113 |
114 | this.currentVersion = lines.get(0).split("\\s")[0];
115 | this.parsedCurrentVersion = Version.parse(this.currentVersion);
116 | this.currentCompatibilityVersion = this.state.getCompatibilityVersions().floor(this.parsedCurrentVersion);
117 | this.writeState();
118 | } catch (IllegalArgumentException | IOException e) {
119 | throw new ModInstallerException("Could not read TLD version.", e);
120 | }
121 | }
122 |
123 | private CompatibilityState readState() {
124 | try {
125 | Path path = this.getStatePath();
126 | if (Files.exists(path)) {
127 | return JsonUtils.deserialize(path, CompatibilityState.class);
128 | }
129 | } catch (IOException e) {
130 | // ignore
131 | }
132 |
133 | try (InputStream inputStream = this.getClass().getResourceAsStream("/default-compatibility-state.json")) {
134 | return JsonUtils.deserialize(inputStream, CompatibilityState.class);
135 | } catch (IOException e) {
136 | // ignore;
137 | }
138 |
139 | return new CompatibilityState();
140 | }
141 |
142 | private void writeState() {
143 | try {
144 | Path path = this.getStatePath();
145 | Files.createDirectories(path.getParent());
146 | JsonUtils.serialize(path, this.state);
147 | } catch (IOException e) {
148 | // ignore
149 | }
150 | }
151 |
152 | public enum Compatibility {
153 | UNKNOWN, OLD, OK;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityState.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.compatibility;
2 |
3 | import java.util.Date;
4 |
5 | public class CompatibilityState {
6 |
7 | private CompatibilityVersions compatibilityVersions;
8 | private String etag;
9 | private Date checked;
10 |
11 | public Date getChecked() {
12 | return this.checked;
13 | }
14 |
15 | public CompatibilityVersions getCompatibilityVersions() {
16 | return this.compatibilityVersions;
17 | }
18 |
19 | public String getEtag() {
20 | return this.etag;
21 | }
22 |
23 | public void setChecked(Date checked) {
24 | this.checked = checked;
25 | }
26 |
27 | public void setCompatibilityVersions(CompatibilityVersions compatibilityVersions) {
28 | this.compatibilityVersions = compatibilityVersions;
29 | }
30 |
31 | public void setEtag(String etag) {
32 | this.etag = etag;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityVersion.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.compatibility;
2 |
3 | import java.util.Date;
4 |
5 | import com.fasterxml.jackson.annotation.JsonIgnore;
6 |
7 | import me.wulfmarius.modinstaller.Version;
8 |
9 | public class CompatibilityVersion implements Comparable {
10 |
11 | private String version;
12 | private Date date;
13 |
14 | @JsonIgnore
15 | private transient Version parsedVersion;
16 |
17 | @Override
18 | public int compareTo(CompatibilityVersion result) {
19 | return this.getParsedVersion().compareTo(result.getParsedVersion());
20 | }
21 |
22 | public Date getDate() {
23 | return this.date;
24 | }
25 |
26 | public Version getParsedVersion() {
27 | if (this.parsedVersion == null) {
28 | this.parsedVersion = Version.parse(this.version);
29 | }
30 |
31 | return this.parsedVersion;
32 | }
33 |
34 | public String getVersion() {
35 | return this.version;
36 | }
37 |
38 | public void setDate(Date date) {
39 | this.date = date;
40 | }
41 |
42 | public void setVersion(String version) {
43 | this.version = version;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityVersions.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.compatibility;
2 |
3 | import java.util.Date;
4 | import java.util.function.Predicate;
5 |
6 | import me.wulfmarius.modinstaller.Version;
7 |
8 | public class CompatibilityVersions {
9 |
10 | private CompatibilityVersion[] versions;
11 |
12 | public CompatibilityVersion floor(Date date) {
13 | return this.getMax(tldVersion -> tldVersion.getDate().compareTo(date) <= 0);
14 | }
15 |
16 | public CompatibilityVersion floor(Version version) {
17 | return this.getMax(tldVersion -> tldVersion.getParsedVersion().compareTo(version) <= 0);
18 | }
19 |
20 | public CompatibilityVersion[] getVersions() {
21 | return this.versions;
22 | }
23 |
24 | public void setVersions(CompatibilityVersion[] versions) {
25 | this.versions = versions;
26 | }
27 |
28 | private CompatibilityVersion getMax(Predicate filter) {
29 | CompatibilityVersion result = null;
30 |
31 | for (CompatibilityVersion eachTldVersion : this.versions) {
32 | if (!filter.test(eachTldVersion)) {
33 | continue;
34 | }
35 |
36 | if (result == null || eachTldVersion.compareTo(result) > 0) {
37 | result = eachTldVersion;
38 | }
39 | }
40 |
41 | return result;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/DependencyResolution.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 |
5 | import me.wulfmarius.modinstaller.*;
6 |
7 | public class DependencyResolution {
8 |
9 | private ModDependencies requested;
10 | private ModDefinition bestMatch;
11 | private Set available = new HashSet<>();
12 |
13 | public static DependencyResolution empty() {
14 | return new DependencyResolution();
15 | }
16 |
17 | public Set getAvailable() {
18 | return this.available;
19 | }
20 |
21 | public ModDefinition getBestMatch() {
22 | return this.bestMatch;
23 | }
24 |
25 | public ModDependencies getRequested() {
26 | return this.requested;
27 | }
28 |
29 | public boolean isAvailable() {
30 | return this.available != null && !this.available.isEmpty();
31 | }
32 |
33 | public boolean isResolved() {
34 | return this.bestMatch != null;
35 | }
36 |
37 | public void setAvailable(Set available) {
38 | this.available = available;
39 | }
40 |
41 | public void setBestMatch(ModDefinition bestMatch) {
42 | this.bestMatch = bestMatch;
43 | }
44 |
45 | public void setRequested(ModDependencies requested) {
46 | this.requested = requested;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/DownloadResponseExtractor.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.io.*;
4 | import java.nio.charset.*;
5 | import java.nio.file.*;
6 | import java.util.regex.*;
7 |
8 | import org.springframework.http.MediaType;
9 | import org.springframework.http.client.ClientHttpResponse;
10 | import org.springframework.lang.Nullable;
11 | import org.springframework.util.StreamUtils;
12 | import org.springframework.web.client.ResponseExtractor;
13 |
14 | import me.wulfmarius.modinstaller.ProgressListeners;
15 | import me.wulfmarius.modinstaller.utils.StringUtils;
16 |
17 | public class DownloadResponseExtractor implements ResponseExtractor {
18 |
19 | private final Path targetFile;
20 | private final ProgressListeners progressListeners;
21 |
22 | public DownloadResponseExtractor(Path targetFile, ProgressListeners progressListeners) {
23 | super();
24 |
25 | this.targetFile = targetFile;
26 | this.progressListeners = progressListeners;
27 | }
28 |
29 | private static String getBody(ClientHttpResponse response) throws IOException {
30 | return StreamUtils.copyToString(response.getBody(), getContentTypeCharset(response.getHeaders().getContentType()));
31 | }
32 |
33 | private static Charset getContentTypeCharset(@Nullable MediaType contentType) {
34 | if (contentType != null && contentType.getCharset() != null) {
35 | return contentType.getCharset();
36 | }
37 |
38 | return StandardCharsets.ISO_8859_1;
39 | }
40 |
41 | @Override
42 | public String extractData(ClientHttpResponse response) throws IOException {
43 | MediaType contentType = response.getHeaders().getContentType();
44 | if (contentType.isCompatibleWith(MediaType.TEXT_HTML)) {
45 | String body = getBody(response);
46 | Pattern pattern = Pattern.compile("\\Qwindow.location.href=\"\\E(\\Qhttp://www.moddb.com/downloads/\\E.*?)\"",
47 | Pattern.CASE_INSENSITIVE);
48 | Matcher matcher = pattern.matcher(body);
49 | if (matcher.find()) {
50 | return matcher.group(1);
51 | }
52 |
53 | throw new SourceException("Received unexpected text/html response.");
54 | }
55 |
56 | long contentLength = response.getHeaders().getContentLength();
57 | this.progressListeners.detail(StringUtils.formatByteCount(contentLength));
58 |
59 | Files.createDirectories(this.targetFile.getParent());
60 | long copied = 0;
61 | this.progressListeners.stepProgress((int) copied, (int) contentLength);
62 |
63 | byte[] buffer = new byte[4096];
64 | try (InputStream inputStream = response.getBody(); OutputStream outputStream = Files.newOutputStream(this.targetFile)) {
65 | while (true) {
66 | int count = inputStream.read(buffer);
67 | if (count == -1) {
68 | break;
69 | }
70 |
71 | copied += count;
72 | outputStream.write(buffer, 0, count);
73 | this.progressListeners.stepProgress((int) copied, (int) contentLength);
74 | }
75 | }
76 |
77 | return null;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/Installation.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 |
5 | import me.wulfmarius.modinstaller.ModDefinition;
6 |
7 | public class Installation {
8 |
9 | private String sourceDefinition;
10 | private String name;
11 | private String version;
12 | private List assets = new ArrayList<>();
13 |
14 | public void addAsset(String asset) {
15 | if (asset == null) {
16 | return;
17 | }
18 |
19 | this.assets.add(asset);
20 | }
21 |
22 | public List getAssets() {
23 | return this.assets;
24 | }
25 |
26 | public String getDisplayName() {
27 | return this.name + " " + this.version;
28 | }
29 |
30 | public String getName() {
31 | return this.name;
32 | }
33 |
34 | public String getSourceDefinition() {
35 | return this.sourceDefinition;
36 | }
37 |
38 | public String getVersion() {
39 | return this.version;
40 | }
41 |
42 | public boolean isAssetReferenced(String asset) {
43 | return this.assets.contains(asset);
44 | }
45 |
46 | public boolean matches(ModDefinition modDefinition) {
47 | return this.name.equals(modDefinition.getName()) && this.version.equals(modDefinition.getVersion());
48 | }
49 |
50 | public void setAssets(List assets) {
51 | this.assets = assets;
52 | }
53 |
54 | public void setName(String name) {
55 | this.name = name;
56 | }
57 |
58 | public void setSourceDefinition(String sourceDefinition) {
59 | this.sourceDefinition = sourceDefinition;
60 | }
61 |
62 | public void setVersion(String version) {
63 | this.version = version;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/Installations.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 | import java.util.stream.*;
5 |
6 | import me.wulfmarius.modinstaller.ModDefinition;
7 |
8 | public class Installations implements Iterable {
9 |
10 | private List installations = new ArrayList<>();
11 |
12 | public static Installations create(Installation... installation) {
13 | Installations result = new Installations();
14 |
15 | if (installation != null) {
16 | for (Installation eachInstallation : installation) {
17 | result.addInstallation(eachInstallation);
18 | }
19 | }
20 |
21 | return result;
22 | }
23 |
24 | public static Installations merge(Installations definitions1, Installations definitions2) {
25 | Installations result = new Installations();
26 |
27 | result.addInstallations(definitions1);
28 | result.addInstallations(definitions2);
29 |
30 | return result;
31 | }
32 |
33 | private static Collector toInstallations() {
34 | return Collectors.reducing(new Installations(), Installations::create, Installations::merge);
35 | }
36 |
37 | public void addInstallation(Installation installation) {
38 | this.installations.add(installation);
39 | }
40 |
41 | public void addInstallations(Iterable otherInstallations) {
42 | for (Installation eachInstallation : otherInstallations) {
43 | this.addInstallation(eachInstallation);
44 | }
45 | }
46 |
47 | public boolean contains(Installation installation) {
48 | return this.installations.contains(installation);
49 | }
50 |
51 | public boolean contains(ModDefinition modDefinition) {
52 | return this.installations.stream().anyMatch(installation -> installation.matches(modDefinition));
53 | }
54 |
55 | public List getInstallations() {
56 | return this.installations;
57 | }
58 |
59 | public Installations getInstallations(String name) {
60 | return this.installations.stream().filter(installation -> installation.getName().equals(name)).collect(toInstallations());
61 | }
62 |
63 | public Installations getInstallationsWithAsset(String asset) {
64 | return this.installations.stream().filter(installation -> installation.isAssetReferenced(asset)).collect(toInstallations());
65 | }
66 |
67 | public int getSize() {
68 | if (this.installations == null) {
69 | return 0;
70 | }
71 |
72 | return this.installations.size();
73 | }
74 |
75 | public boolean isEmpty() {
76 | return this.installations == null || this.installations.isEmpty();
77 | }
78 |
79 | @Override
80 | public Iterator iterator() {
81 | return this.installations.iterator();
82 | }
83 |
84 | public void remove(Installation installation) {
85 | this.installations.remove(installation);
86 | }
87 |
88 | public void remove(ModDefinition modDefinition) {
89 | this.installations.removeIf(installation -> installation.matches(modDefinition));
90 | }
91 |
92 | public void setInstallations(List installations) {
93 | this.installations = installations;
94 | }
95 |
96 | public Stream stream() {
97 | return this.installations.stream();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/RateLimitException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.time.Instant;
4 |
5 | public class RateLimitException extends RuntimeException {
6 |
7 | private static final long serialVersionUID = 1L;
8 |
9 | private final Instant reset;
10 |
11 | public RateLimitException(Instant reset) {
12 | super();
13 | this.reset = reset;
14 | }
15 |
16 | public Instant getReset() {
17 | return this.reset;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/Repository.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import static me.wulfmarius.modinstaller.repository.SourceFactory.PARAMETER_ETAG;
4 |
5 | import java.io.*;
6 | import java.nio.file.*;
7 | import java.util.*;
8 | import java.util.stream.Collectors;
9 |
10 | import org.springframework.http.*;
11 | import org.springframework.util.StringUtils;
12 |
13 | import me.wulfmarius.modinstaller.*;
14 | import me.wulfmarius.modinstaller.ProgressListener.StepType;
15 | import me.wulfmarius.modinstaller.repository.source.*;
16 | import me.wulfmarius.modinstaller.rest.RestClient;
17 | import me.wulfmarius.modinstaller.utils.JsonUtils;
18 |
19 | public class Repository {
20 |
21 | private static final String SNAPSHOT_URL = "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/src/main/resources/default-sources.json";
22 |
23 | private final Path basePath;
24 |
25 | private final Sources sources = new Sources();
26 | private final List sourceFactories = new ArrayList<>();
27 | private final ProgressListeners progressListeners = new ProgressListeners();
28 | private final SourcesChangedListeners sourcesChangedListeners = new SourcesChangedListeners();
29 |
30 | public Repository(Path basePath) {
31 | super();
32 |
33 | this.basePath = basePath;
34 | try {
35 | Files.createDirectories(basePath);
36 | } catch (IOException e) {
37 | throw new RepositoryException("Could not create base path " + basePath + ".", e);
38 | }
39 | }
40 |
41 | public static String getFileName(Asset asset) {
42 | String url = asset.getUrl();
43 |
44 | int index = url.indexOf("?");
45 | if (index != -1) {
46 | url = url.substring(0, index);
47 | }
48 |
49 | index = url.lastIndexOf('/');
50 | if (index == url.length() - 1) {
51 | url = url.substring(0, url.length() - 1);
52 | index = url.lastIndexOf('/');
53 | }
54 |
55 | if (index != -1) {
56 | url = url.substring(index + 1);
57 | }
58 |
59 | if (StringUtils.isEmpty(asset.getType())) {
60 | return url;
61 | }
62 |
63 | return url;
64 | }
65 |
66 | public void addProgressListener(ProgressListener listener) {
67 | this.progressListeners.addListener(listener);
68 | }
69 |
70 | public void addSourcesChangedListener(SourcesChangedListener listener) {
71 | this.sourcesChangedListeners.addListener(listener);
72 | }
73 |
74 | public void downloadAssets(ModDefinition modDefinition) {
75 | Asset[] assets = modDefinition.getAssets();
76 |
77 | for (Asset eachAsset : assets) {
78 | Path assetPath = this.getAssetPath(modDefinition, eachAsset);
79 | if (Files.notExists(assetPath)) {
80 | RestClient.getInstance().downloadAsset(eachAsset.getUrl(), assetPath, this.progressListeners);
81 | }
82 | }
83 | }
84 |
85 | public Path getAssetPath(ModDefinition modDefinition, Asset asset) {
86 | return this.basePath.resolve(modDefinition.getName()).resolve(modDefinition.getVersion()).resolve(getFileName(asset));
87 | }
88 |
89 | public List getLatestVersions() {
90 | return this.getSources().stream().flatMap(Source::getLatestVersions).collect(Collectors.toList());
91 | }
92 |
93 | public ModDefinitions getMatching(ModDependency modDependency) {
94 | ModDefinitions result = new ModDefinitions();
95 |
96 | for (Source eachSource : this.sources) {
97 | result.addModDefinitions(eachSource.getMatchingDefinitions(modDependency));
98 | }
99 |
100 | return result;
101 | }
102 |
103 | public Optional getModDefinition(String name, String version) {
104 | for (Source eachSource : this.sources) {
105 | Optional modDefinition = eachSource.getModDefinition(name, version);
106 | if (modDefinition.isPresent()) {
107 | return modDefinition;
108 | }
109 | }
110 |
111 | return Optional.empty();
112 | }
113 |
114 | public List getModDefinitions(String name) {
115 | return this.sources.stream()
116 | .flatMap(Source::getModDefinitionStream)
117 | .filter(modDefinition -> modDefinition.getName().equals(name))
118 | .sorted(ModDefinition::latest)
119 | .collect(Collectors.toList());
120 | }
121 |
122 | public Sources getSources() {
123 | return this.sources;
124 | }
125 |
126 | public void initialize() {
127 | this.sourceFactories.add(new GithubSourceFactory(RestClient.getInstance()));
128 | this.sourceFactories.add(new DirectSourceFactory(RestClient.getInstance()));
129 | this.sourceFactories.add(new FileSourceFactory());
130 |
131 | Sources savedSources = this.readSources();
132 | if (!savedSources.isEmpty()) {
133 | this.sources.addSources(savedSources);
134 | this.sources.setLastUpdate(savedSources.getLastUpdate());
135 | this.sources.setSnapshotETag(savedSources.getSnapshotETag());
136 | this.sourcesChangedListeners.changed();
137 | }
138 | }
139 |
140 | public void invalidateSources() {
141 | for (Source eachSource : this.sources) {
142 | eachSource.removeParameter(SourceFactory.PARAMETER_ETAG);
143 | }
144 | }
145 |
146 | public void refreshSnapshot() {
147 | ResponseEntity response = RestClient.getInstance().fetch(SNAPSHOT_URL, this.sources.getSnapshotETag());
148 |
149 | if (response.getStatusCode() == HttpStatus.NOT_MODIFIED) {
150 | return;
151 | }
152 |
153 | if (!response.getStatusCode().is2xxSuccessful()) {
154 | this.progressListeners.error("Could not find snapshot: " + SNAPSHOT_URL + " " + response.getStatusCodeValue() + "/"
155 | + response.getStatusCode().getReasonPhrase());
156 | return;
157 | }
158 |
159 | Sources snapshot = RestClient.getInstance().deserialize(response, Sources.class, Sources::new);
160 | this.applySnapshot(snapshot);
161 |
162 | this.sources.setSnapshotETag(response.getHeaders().getETag());
163 | this.writeSources();
164 | }
165 |
166 | public void refreshSources() {
167 | String changes = null;
168 |
169 | try {
170 | this.progressListeners.started("Refreshing Sources");
171 |
172 | List previousLatestVersions = this.getLatestVersions();
173 |
174 | this.performRefreshSources();
175 |
176 | List currentLatestVersions = this.getLatestVersions();
177 | currentLatestVersions.removeAll(previousLatestVersions);
178 | if (!currentLatestVersions.isEmpty()) {
179 | changes = currentLatestVersions.stream().map(ModDefinition::getName).collect(
180 | Collectors.joining("\n\t", "\n\nThe following mods were added/updated:\n\t", "\n"));
181 | } else {
182 | changes = "\n\nNo changes found";
183 | }
184 | } catch (AbortException e) {
185 | this.progressListeners.error(e.getMessage());
186 | this.progressListeners.detail("Aborting now.");
187 | } finally {
188 | this.writeSources();
189 | this.progressListeners.finished(changes);
190 | }
191 | }
192 |
193 | public void registerSource(String definition) {
194 | try {
195 | this.progressListeners.started("Add " + definition);
196 | this.performRegisterSource(definition);
197 | } catch (AbortException e) {
198 | this.progressListeners.error(e.getMessage());
199 | this.progressListeners.detail("Aborting now.");
200 | } finally {
201 | this.writeSources();
202 | this.progressListeners.stepProgress(1, 1);
203 | this.progressListeners.finished();
204 | }
205 | }
206 |
207 | public void removeProgressListener(ProgressListener listener) {
208 | this.progressListeners.removeListener(listener);
209 | }
210 |
211 | public void removeSourcesChangedListener(SourcesChangedListener listener) {
212 | this.sourcesChangedListeners.removeListener(listener);
213 | }
214 |
215 | private void addSource(Source source) {
216 | this.sources.addSource(source);
217 | }
218 |
219 | private void applySnapshot(Sources snapshot) {
220 | for (Source eachSnapshotSource : snapshot) {
221 | if (!this.sources.contains(eachSnapshotSource.getDefinition())) {
222 | this.progressListeners.detail("Adding " + eachSnapshotSource.getDefinition());
223 | this.addSource(eachSnapshotSource);
224 | continue;
225 | }
226 |
227 | Date now = new Date(0);
228 | this.sources.stream()
229 | .filter(eachSource -> eachSource.getDefinition().equals(eachSnapshotSource.getDefinition()))
230 | .filter(eachSource -> !eachSource.getParameter(PARAMETER_ETAG).equals(eachSnapshotSource.getParameter(PARAMETER_ETAG)))
231 | .filter(eachSource -> !eachSource.getLastUpdated().orElse(now).after(eachSnapshotSource.getLastUpdated().orElse(now)))
232 | .findFirst()
233 | .ifPresent(eachSource -> {
234 | this.progressListeners.detail("Updating " + eachSource.getDefinition());
235 | eachSource.update(eachSnapshotSource);
236 | });
237 | }
238 | }
239 |
240 | private Source createSource(String sourceDefinition, Map parameters) {
241 | for (SourceFactory eachSourceFactory : this.sourceFactories) {
242 | if (!eachSourceFactory.isSupportedSource(sourceDefinition)) {
243 | continue;
244 | }
245 |
246 | this.progressListeners.detail("Loading definition");
247 | return eachSourceFactory.create(sourceDefinition, parameters);
248 | }
249 |
250 | throw new SourceException("Unsupported source '" + sourceDefinition + "'.");
251 | }
252 |
253 | private Path getSourcesPath() {
254 | return this.basePath.resolve("sources.json");
255 | }
256 |
257 | private void performRefreshSources() {
258 | int refreshed = 0;
259 | int total = this.sources.size();
260 |
261 | for (int i = 0; i < total; i++) {
262 | try {
263 | this.refreshSource(this.sources.getSources().get(i));
264 | } catch (AbortException e) {
265 | throw e;
266 | } catch (Exception e) {
267 | this.progressListeners.error(e.getMessage());
268 | }
269 | this.progressListeners.stepProgress(++refreshed, total);
270 | }
271 | }
272 |
273 | private void performRegisterSource(String definition) {
274 | this.progressListeners.stepStarted(definition, StepType.ADD);
275 |
276 | if (this.sources.contains(definition)) {
277 | this.progressListeners.detail("Already present.");
278 | return;
279 | }
280 |
281 | try {
282 | Source source = this.createSource(definition, Collections.emptyMap());
283 | this.registerDefinitions(source);
284 | this.addSource(source);
285 | } catch (AbortException e) {
286 | throw e;
287 | } catch (RuntimeException e) {
288 | this.progressListeners.detail("Could not register source: " + e);
289 | }
290 | }
291 |
292 | private Sources readSources() {
293 | try {
294 | Path sourcesPath = this.getSourcesPath();
295 | if (Files.exists(sourcesPath)) {
296 | return JsonUtils.deserialize(sourcesPath, Sources.class);
297 | }
298 | } catch (IOException e) {
299 | throw new RepositoryException("Failed to read sources.", e);
300 | }
301 |
302 | try (InputStream inputStream = this.getClass().getResourceAsStream("/default-sources.json")) {
303 | return JsonUtils.deserialize(inputStream, Sources.class);
304 | } catch (IOException e) {
305 | // ignore
306 | }
307 |
308 | return new Sources();
309 | }
310 |
311 | private boolean refreshSource(Source source) {
312 | this.progressListeners.stepStarted(source.getDefinition(), StepType.REFRESH);
313 |
314 | Source refreshedSource = this.createSource(source.getDefinition(), source.getParameters());
315 | if (refreshedSource.hasParameterValue(SourceFactory.PARAMETER_UNMODIFIED, "true")) {
316 | this.progressListeners.detail("Unmodified");
317 |
318 | // it may be possible that this source's definitions were not added last time (because of an error)
319 | // so retry to register them now
320 | this.registerDefinitions(source);
321 |
322 | return false;
323 | }
324 |
325 | source.update(refreshedSource);
326 | this.registerDefinitions(refreshedSource);
327 |
328 | this.progressListeners.detail("Updated");
329 | return true;
330 | }
331 |
332 | private void registerDefinitions(Source source) {
333 | String[] definitions = source.getDefinitions();
334 | if (definitions == null) {
335 | return;
336 | }
337 |
338 | for (String eachDefinition : definitions) {
339 | if (this.sources.contains(eachDefinition)) {
340 | continue;
341 | }
342 |
343 | this.performRegisterSource(eachDefinition);
344 | }
345 | }
346 |
347 | private void writeSources() {
348 | try {
349 | this.sources.setLastUpdate(new Date());
350 | JsonUtils.serialize(this.getSourcesPath(), this.sources);
351 | this.sourcesChangedListeners.changed();
352 | } catch (IOException e) {
353 | this.progressListeners.error("Could not save sources: " + e);
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/RepositoryException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import me.wulfmarius.modinstaller.ModInstallerException;
4 |
5 | public class RepositoryException extends ModInstallerException {
6 |
7 | private static final long serialVersionUID = 1L;
8 |
9 | public RepositoryException(String message) {
10 | super(message);
11 | }
12 |
13 | public RepositoryException(String message, Throwable cause) {
14 | super(message, cause);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/Source.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 | import java.util.stream.*;
5 |
6 | import org.springframework.util.StringUtils;
7 |
8 | import me.wulfmarius.modinstaller.*;
9 |
10 | public class Source {
11 |
12 | public static final String VERSION = "3";
13 |
14 | private String definition;
15 | private String name;
16 | private String url;
17 | private String description;
18 | private String[] definitions;
19 |
20 | private final Map parameters = new HashMap<>();
21 |
22 | private ModDefinitions modDefinitions = new ModDefinitions();
23 |
24 | public static Source from(String definition, SourceDescription sourceDescription) {
25 | Source result = new Source();
26 |
27 | result.setDefinition(definition);
28 | result.setName(sourceDescription.getName());
29 | result.setUrl(sourceDescription.getUrl());
30 | result.setDescription(sourceDescription.getDescription());
31 | result.setDefinitions(sourceDescription.getDefinitions());
32 | result.parameters.putAll(sourceDescription.getParameters());
33 | result.createModDefinitions(sourceDescription.getReleases());
34 |
35 | return result;
36 | }
37 |
38 | public String getDefinition() {
39 | return this.definition;
40 | }
41 |
42 | public String[] getDefinitions() {
43 | return this.definitions;
44 | }
45 |
46 | public String getDescription() {
47 | return this.description;
48 | }
49 |
50 | public Optional getLastUpdated() {
51 | return this.getModDefinitionStream().map(ModDefinition::getLastUpdated).max(Date::compareTo);
52 | }
53 |
54 | public Stream getLatestVersions() {
55 | Map> collect = this.modDefinitions.stream()
56 | .collect(Collectors.groupingBy(ModDefinition::getName, Collectors.minBy(ModDefinition::latest)));
57 | return collect.values().stream().map(Optional::get);
58 | }
59 |
60 | public ModDefinitions getMatchingDefinitions(ModDependency dependency) {
61 | return this.modDefinitions.getMatchingDefinitions(dependency);
62 | }
63 |
64 | public Optional getModDefinition(String modDefinitionName, String modDefinitionVersion) {
65 | return this.modDefinitions.getModDefinition(modDefinitionName, modDefinitionVersion);
66 | }
67 |
68 | public ModDefinitions getModDefinitions() {
69 | return this.modDefinitions;
70 | }
71 |
72 | public Stream getModDefinitionStream() {
73 | return this.modDefinitions.stream();
74 | }
75 |
76 | public String getName() {
77 | return this.name;
78 | }
79 |
80 | public String getParameter(String parameterName) {
81 | if (this.parameters == null) {
82 | return null;
83 | }
84 |
85 | return this.parameters.get(parameterName);
86 | }
87 |
88 | public Map getParameters() {
89 | return this.parameters;
90 | }
91 |
92 | public String getUrl() {
93 | return this.url;
94 | }
95 |
96 | public boolean hasParameterValue(String parameterName, String value) {
97 | if (value == null) {
98 | return this.getParameter(parameterName) == null;
99 | }
100 |
101 | return value.equals(this.getParameter(parameterName));
102 | }
103 |
104 | public void removeParameter(String parameterName) {
105 | this.parameters.remove(parameterName);
106 | }
107 |
108 | public void setDefinition(String definition) {
109 | this.definition = definition;
110 | }
111 |
112 | public void setDefinitions(String[] definitions) {
113 | this.definitions = definitions;
114 | }
115 |
116 | public void setDescription(String description) {
117 | this.description = description;
118 | }
119 |
120 | public void setModDefinitions(ModDefinitions modDefinitions) {
121 | this.modDefinitions = modDefinitions;
122 | }
123 |
124 | public void setName(String name) {
125 | this.name = name;
126 | }
127 |
128 | public void setUrl(String url) {
129 | this.url = url;
130 | }
131 |
132 | public void update(Source refreshedSource) {
133 | this.name = refreshedSource.name;
134 | this.description = refreshedSource.description;
135 | this.modDefinitions = refreshedSource.modDefinitions;
136 |
137 | this.parameters.clear();
138 | this.parameters.putAll(refreshedSource.parameters);
139 | }
140 |
141 | private void createModDefinitions(ModDefinition[] releases) {
142 | if (releases == null) {
143 | return;
144 | }
145 |
146 | for (ModDefinition eachModDefinition : releases) {
147 | if (StringUtils.isEmpty(eachModDefinition.getName())) {
148 | eachModDefinition.setName(this.name);
149 | }
150 |
151 | if (StringUtils.isEmpty(eachModDefinition.getUrl())) {
152 | eachModDefinition.setUrl(this.url);
153 | }
154 | if (StringUtils.isEmpty(eachModDefinition.getUrl())) {
155 | eachModDefinition.setUrl(this.definition);
156 | }
157 |
158 | if (StringUtils.isEmpty(eachModDefinition.getDescription())) {
159 | eachModDefinition.setDescription(this.description);
160 | }
161 |
162 | eachModDefinition.setLastUpdated(new Date());
163 |
164 | this.modDefinitions.addModDefinition(eachModDefinition);
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/SourceDescription.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 |
5 | import me.wulfmarius.modinstaller.ModDefinition;
6 |
7 | public class SourceDescription {
8 |
9 | private String name;
10 | private String author;
11 | private String url;
12 | private String description;
13 | private ModDefinition[] releases;
14 | private String[] definitions;
15 | private Map parameters = new HashMap<>();
16 |
17 | public String getAuthor() {
18 | return this.author;
19 | }
20 |
21 | public String[] getDefinitions() {
22 | return this.definitions;
23 | }
24 |
25 | public String getDescription() {
26 | return this.description;
27 | }
28 |
29 | public String getName() {
30 | return this.name;
31 | }
32 |
33 | public Map getParameters() {
34 | return this.parameters;
35 | }
36 |
37 | public ModDefinition[] getReleases() {
38 | return this.releases;
39 | }
40 |
41 | public String getUrl() {
42 | return this.url;
43 | }
44 |
45 | public void setAuthor(String author) {
46 | this.author = author;
47 | }
48 |
49 | public void setDefinitions(String[] definitions) {
50 | this.definitions = definitions;
51 | }
52 |
53 | public void setDescription(String description) {
54 | this.description = description;
55 | }
56 |
57 | public void setName(String name) {
58 | this.name = name;
59 | }
60 |
61 | public void setParameter(String name, String value) {
62 | this.parameters.put(name, value);
63 | }
64 |
65 | public void setParameters(Map parameters) {
66 | this.parameters = parameters;
67 | }
68 |
69 | public void setReleases(ModDefinition[] releases) {
70 | this.releases = releases;
71 | }
72 |
73 | public void setUrl(String url) {
74 | this.url = url;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/SourceException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | public class SourceException extends RepositoryException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | public SourceException(String message) {
8 | super(message);
9 | }
10 |
11 | public SourceException(String message, Throwable cause) {
12 | super(message, cause);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/SourceFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.Map;
4 |
5 | public interface SourceFactory {
6 |
7 | String PARAMETER_UNMODIFIED = "unmodified";
8 | String PARAMETER_ETAG = "etag";
9 | String PARAMETER_VERSION = "version";
10 |
11 | Source create(String sourceDefinition, Map parameters);
12 |
13 | boolean isSupportedSource(String sourceDefinition);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/Sources.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository;
2 |
3 | import java.util.*;
4 | import java.util.stream.Stream;
5 |
6 | public class Sources implements Iterable {
7 |
8 | private List sources = new ArrayList<>();
9 |
10 | private Date lastUpdate;
11 | private String snapshotETag;
12 |
13 | public void addSource(Source source) {
14 | this.sources.add(source);
15 | }
16 |
17 | public void addSources(Iterable otherSources) {
18 | for (Source eachSource : otherSources) {
19 | this.addSource(eachSource);
20 | }
21 | }
22 |
23 | public boolean contains(String definition) {
24 | return this.sources.stream().anyMatch(source -> definition.equalsIgnoreCase(source.getDefinition()));
25 | }
26 |
27 | public Date getLastUpdate() {
28 | return this.lastUpdate;
29 | }
30 |
31 | public String getSnapshotETag() {
32 | return this.snapshotETag;
33 | }
34 |
35 | public List getSources() {
36 | return this.sources;
37 | }
38 |
39 | public boolean isEmpty() {
40 | return this.sources == null || this.sources.isEmpty();
41 | }
42 |
43 | @Override
44 | public Iterator iterator() {
45 | return this.sources.iterator();
46 | }
47 |
48 | public void removeSource(Source source) {
49 | this.sources.remove(source);
50 | }
51 |
52 | public void setLastUpdate(Date lastUpdate) {
53 | this.lastUpdate = lastUpdate;
54 | }
55 |
56 | public void setSnapshotETag(String snapshotETag) {
57 | this.snapshotETag = snapshotETag;
58 | }
59 |
60 | public void setSources(List sources) {
61 | this.sources = sources;
62 | }
63 |
64 | public int size() {
65 | if (this.sources == null) {
66 | return 0;
67 | }
68 |
69 | return this.sources.size();
70 | }
71 |
72 | public Stream stream() {
73 | if (this.sources == null) {
74 | return Stream.empty();
75 | }
76 |
77 | return this.sources.stream();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/AbstractSourceFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import java.util.Map;
4 |
5 | import org.springframework.http.ResponseEntity;
6 | import org.springframework.util.StringUtils;
7 |
8 | import me.wulfmarius.modinstaller.repository.*;
9 | import me.wulfmarius.modinstaller.rest.RestClient;
10 |
11 | public abstract class AbstractSourceFactory implements SourceFactory {
12 |
13 | protected final RestClient restClient;
14 |
15 | protected AbstractSourceFactory(RestClient restClient) {
16 | super();
17 | this.restClient = restClient;
18 | }
19 |
20 | @Override
21 | public final Source create(String definition, Map parameters) {
22 | if (!this.isSupportedSource(definition)) {
23 | throw new IllegalArgumentException("Unsupported source definition " + definition);
24 | }
25 |
26 | SourceDescription sourceDescription = this.getSourceDescription(definition, parameters);
27 | this.postProcessSourceDescription(definition, sourceDescription);
28 | return Source.from(definition, sourceDescription);
29 | }
30 |
31 | protected abstract String getDefinitionsUrl(String sourceDefinition);
32 |
33 | protected SourceDescription getSourceDescription(String sourceDefinition, Map parameters) {
34 | String url = this.getDefinitionsUrl(sourceDefinition);
35 |
36 | ResponseEntity response = this.restClient.fetch(url, parameters.get(PARAMETER_ETAG));
37 | if (!response.getStatusCode().isError()) {
38 | SourceDescription result = this.restClient.deserialize(response, SourceDescription.class,
39 | this::createUnmodifiedSourceDescription);
40 |
41 | result.setParameter(PARAMETER_ETAG, response.getHeaders().getETag());
42 | result.setParameter(PARAMETER_VERSION, Source.VERSION);
43 |
44 | return result;
45 | }
46 |
47 | throw new SourceException("Could not read source description: " + response.getStatusCodeValue() + ", " + response.getBody());
48 | }
49 |
50 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) {
51 | if (StringUtils.isEmpty(sourceDescription.getUrl())) {
52 | sourceDescription.setUrl(definition);
53 | }
54 | }
55 |
56 | private SourceDescription createUnmodifiedSourceDescription() {
57 | SourceDescription result = new SourceDescription();
58 | result.setParameter(PARAMETER_UNMODIFIED, "true");
59 | return result;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/DirectSourceFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import me.wulfmarius.modinstaller.ModDefinition;
4 | import me.wulfmarius.modinstaller.repository.SourceDescription;
5 | import me.wulfmarius.modinstaller.rest.RestClient;
6 |
7 | public class DirectSourceFactory extends AbstractSourceFactory {
8 |
9 | public DirectSourceFactory(RestClient restClient) {
10 | super(restClient);
11 | }
12 |
13 | @Override
14 | public boolean isSupportedSource(String definition) {
15 | return definition.startsWith("http://") || definition.startsWith("https://");
16 | }
17 |
18 | @Override
19 | protected String getDefinitionsUrl(String sourceDefinition) {
20 | return sourceDefinition;
21 | }
22 |
23 | @Override
24 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) {
25 | super.postProcessSourceDescription(definition, sourceDescription);
26 |
27 | ModDefinition[] releases = sourceDescription.getReleases();
28 | if (releases == null) {
29 | return;
30 | }
31 |
32 | for (ModDefinition eachRelease : releases) {
33 | if (eachRelease.getAuthor() == null) {
34 | eachRelease.setAuthor(sourceDescription.getAuthor());
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/FileSourceFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import java.io.IOException;
4 | import java.nio.file.Paths;
5 | import java.util.Map;
6 |
7 | import me.wulfmarius.modinstaller.repository.*;
8 | import me.wulfmarius.modinstaller.utils.JsonUtils;
9 |
10 | public class FileSourceFactory extends AbstractSourceFactory {
11 |
12 | public FileSourceFactory() {
13 | super(null);
14 | }
15 |
16 | @Override
17 | public boolean isSupportedSource(String sourceDefinition) {
18 | return true;
19 | }
20 |
21 | @Override
22 | protected String getDefinitionsUrl(String sourceDefinition) {
23 | return null;
24 | }
25 |
26 | @Override
27 | protected SourceDescription getSourceDescription(String sourceDefinition, Map parameters) {
28 | try {
29 | return JsonUtils.deserialize(Paths.get(sourceDefinition), SourceDescription.class);
30 | } catch (IOException e) {
31 | throw new SourceException("Could not read source description: " + e.getMessage());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/GithubAsset.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import com.fasterxml.jackson.annotation.*;
4 |
5 | import me.wulfmarius.modinstaller.Asset;
6 |
7 | @JsonIgnoreProperties(ignoreUnknown = true)
8 | public class GithubAsset {
9 |
10 | private String name;
11 |
12 | @JsonProperty("browser_download_url")
13 | private String downloadUrl;
14 |
15 | public String getDownloadUrl() {
16 | return this.downloadUrl;
17 | }
18 |
19 | public String getName() {
20 | return this.name;
21 | }
22 |
23 | public void setDownloadUrl(String downloadUrl) {
24 | this.downloadUrl = downloadUrl;
25 | }
26 |
27 | public void setName(String name) {
28 | this.name = name;
29 | }
30 |
31 | public Asset toAsset() {
32 | return Asset.withUrl(this.downloadUrl);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/GithubAuthor.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown = true)
6 | public class GithubAuthor {
7 |
8 | private String login;
9 |
10 | public String getLogin() {
11 | return this.login;
12 | }
13 |
14 | public void setLogin(String login) {
15 | this.login = login;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/GithubRelease.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import java.util.Date;
4 |
5 | import com.fasterxml.jackson.annotation.*;
6 |
7 | import me.wulfmarius.modinstaller.Version;
8 |
9 | @JsonIgnoreProperties(ignoreUnknown = true)
10 | public class GithubRelease {
11 |
12 | private String name;
13 |
14 | @JsonProperty("tag_name")
15 | private String tag;
16 |
17 | @JsonProperty("html_url")
18 | private String url;
19 |
20 | @JsonProperty("published_at")
21 | private Date date;
22 |
23 | private GithubAsset[] assets;
24 |
25 | private GithubAuthor author;
26 |
27 | private String body;
28 |
29 | public GithubAsset[] getAssets() {
30 | return this.assets;
31 | }
32 |
33 | public GithubAuthor getAuthor() {
34 | return this.author;
35 | }
36 |
37 | public String getBody() {
38 | return this.body;
39 | }
40 |
41 | public Date getDate() {
42 | return this.date;
43 | }
44 |
45 | public String getName() {
46 | return this.name;
47 | }
48 |
49 | public String getTag() {
50 | return this.tag;
51 | }
52 |
53 | public String getUrl() {
54 | return this.url;
55 | }
56 |
57 | public boolean hasMatchingTag(String tagName) {
58 | if (this.tag == null) {
59 | return false;
60 | }
61 |
62 | if (this.tag.equalsIgnoreCase(tagName)) {
63 | return true;
64 | }
65 |
66 | try {
67 | return Version.parse(this.tag).equals(Version.parse(tagName));
68 | } catch (Exception e) {
69 | return false;
70 | }
71 | }
72 |
73 | public void setAssets(GithubAsset[] assets) {
74 | this.assets = assets;
75 | }
76 |
77 | public void setAuthor(GithubAuthor author) {
78 | this.author = author;
79 | }
80 |
81 | public void setBody(String body) {
82 | this.body = body;
83 | }
84 |
85 | public void setDate(Date date) {
86 | this.date = date;
87 | }
88 |
89 | public void setName(String name) {
90 | this.name = name;
91 | }
92 |
93 | public void setTag(String tag) {
94 | this.tag = tag;
95 | }
96 |
97 | public void setUrl(String url) {
98 | this.url = url;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/repository/source/GithubSourceFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.repository.source;
2 |
3 | import java.text.MessageFormat;
4 | import java.util.*;
5 | import java.util.regex.*;
6 |
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.util.StringUtils;
9 |
10 | import me.wulfmarius.modinstaller.*;
11 | import me.wulfmarius.modinstaller.repository.SourceDescription;
12 | import me.wulfmarius.modinstaller.rest.RestClient;
13 |
14 | public class GithubSourceFactory extends AbstractSourceFactory {
15 |
16 | public static final String PARAMETER_USER = "user";
17 |
18 | private static final Pattern SOURCE_PATTERN = Pattern.compile("\\Qhttps://github.com/\\E([A-Z0-9-]+)/([A-Z0-9-_]+)/?",
19 | Pattern.CASE_INSENSITIVE);
20 |
21 | public GithubSourceFactory(RestClient restClient) {
22 | super(restClient);
23 | }
24 |
25 | @Override
26 | public boolean isSupportedSource(String sourceDefinition) {
27 | return SOURCE_PATTERN.matcher(sourceDefinition).matches();
28 | }
29 |
30 | @Override
31 | protected String getDefinitionsUrl(String sourceDefinition) {
32 | Matcher matcher = SOURCE_PATTERN.matcher(sourceDefinition);
33 | if (!matcher.matches()) {
34 | throw new IllegalArgumentException("Unsupported source definition " + sourceDefinition);
35 | }
36 |
37 | return MessageFormat.format("https://raw.githubusercontent.com/{0}/{1}/master/mod-installer-description.json",
38 | matcher.group(1),
39 | matcher.group(2));
40 | }
41 |
42 | protected GithubRelease[] getGithubReleases(String definition) {
43 | String url = definition.replace("//github.com/", "//api.github.com/repos/") + "/releases";
44 | ResponseEntity response = this.restClient.fetch(url, null);
45 | return this.restClient.deserialize(response, GithubRelease[].class, () -> new GithubRelease[0]);
46 | }
47 |
48 | @Override
49 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) {
50 | super.postProcessSourceDescription(definition, sourceDescription);
51 |
52 | ModDefinition[] releases = sourceDescription.getReleases();
53 | if (releases == null) {
54 | return;
55 | }
56 |
57 | ReleaseProvider releaseProvider = new ReleaseProvider(definition);
58 |
59 | for (ModDefinition eachRelease : releases) {
60 | if (StringUtils.isEmpty(eachRelease.getUrl())) {
61 | eachRelease.setUrl(releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getUrl).orElse(
62 | definition + "/releases/tag/" + eachRelease.getVersion()));
63 | }
64 |
65 | if (StringUtils.isEmpty(eachRelease.getChanges())) {
66 | eachRelease.setChanges(releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getBody).orElse(""));
67 | }
68 |
69 | if (eachRelease.getAssets() == null || eachRelease.getAssets().length == 0) {
70 | eachRelease.setAssets(releaseProvider.getRelease(eachRelease.getVersion())
71 | .map(GithubRelease::getAssets)
72 | .map(assets -> Arrays.stream(assets).map(GithubAsset::toAsset).toArray(Asset[]::new))
73 | .orElse(new Asset[0]));
74 | }
75 |
76 | if (eachRelease.getReleaseDate() == null) {
77 | eachRelease.setReleaseDate(
78 | releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getDate).orElse(new Date()));
79 | }
80 |
81 | if (eachRelease.getAuthor() == null) {
82 | eachRelease.setAuthor(releaseProvider.getRelease(eachRelease.getVersion())
83 | .map(GithubRelease::getAuthor)
84 | .map(GithubAuthor::getLogin)
85 | .orElse(sourceDescription.getAuthor()));
86 | }
87 |
88 | if (eachRelease.getAuthor() == null) {
89 | Matcher matcher = SOURCE_PATTERN.matcher(definition);
90 | if (matcher.matches()) {
91 | eachRelease.setAuthor(matcher.group(1));
92 | }
93 | }
94 | }
95 | }
96 |
97 | private class ReleaseProvider {
98 |
99 | private final String definition;
100 | private GithubRelease[] githubReleases = null;
101 |
102 | public ReleaseProvider(String definition) {
103 | super();
104 | this.definition = definition;
105 | }
106 |
107 | public Optional getRelease(String version) {
108 | if (this.githubReleases == null) {
109 | this.githubReleases = GithubSourceFactory.this.getGithubReleases(this.definition);
110 | }
111 |
112 | return Arrays.stream(this.githubReleases).filter(release -> release.hasMatchingTag(version)).findFirst();
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/rest/HostUnreachableException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.rest;
2 |
3 | import me.wulfmarius.modinstaller.AbortException;
4 |
5 | public class HostUnreachableException extends AbortException {
6 |
7 | private static final long serialVersionUID = 1L;
8 |
9 | public HostUnreachableException(String host) {
10 | super("Unknown host " + host + ". Are you offline?");
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/rest/RateLimitException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.rest;
2 |
3 | import java.time.*;
4 | import java.time.format.*;
5 |
6 | import me.wulfmarius.modinstaller.AbortException;
7 |
8 | public class RateLimitException extends AbortException {
9 |
10 | private static final long serialVersionUID = 1L;
11 |
12 | private final Instant reset;
13 |
14 | public RateLimitException(Instant reset) {
15 | super("RATE LIMIT REACHED. Please try again after "
16 | + DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM).format(reset.atZone(ZoneId.systemDefault())));
17 | this.reset = reset;
18 | }
19 |
20 | public Instant getReset() {
21 | return this.reset;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/rest/RestClient.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.rest;
2 |
3 | import java.io.*;
4 | import java.net.UnknownHostException;
5 | import java.nio.charset.*;
6 | import java.nio.file.Path;
7 | import java.time.Instant;
8 | import java.util.Arrays;
9 | import java.util.function.Supplier;
10 | import java.util.zip.GZIPInputStream;
11 |
12 | import org.springframework.core.NestedRuntimeException;
13 | import org.springframework.http.*;
14 | import org.springframework.http.client.*;
15 | import org.springframework.http.converter.StringHttpMessageConverter;
16 | import org.springframework.util.*;
17 | import org.springframework.web.client.*;
18 |
19 | import me.wulfmarius.modinstaller.ProgressListener.StepType;
20 | import me.wulfmarius.modinstaller.ProgressListeners;
21 | import me.wulfmarius.modinstaller.repository.*;
22 | import me.wulfmarius.modinstaller.utils.JsonUtils;
23 |
24 | public class RestClient {
25 |
26 | private static final RestClient INSTANCE = new RestClient();
27 |
28 | private RestTemplate restTemplate;
29 | private Instant rateLimitReset;
30 |
31 | private RestClient() {
32 | super();
33 |
34 | SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
35 | requestFactory.setConnectTimeout(10000);
36 | requestFactory.setReadTimeout(10000);
37 |
38 | this.restTemplate = new RestTemplate(requestFactory);
39 | this.restTemplate.setMessageConverters(Arrays.asList(new StringHttpMessageConverter()));
40 | }
41 |
42 | public static RestClient getInstance() {
43 | return INSTANCE;
44 | }
45 |
46 | public T deserialize(ResponseEntity response, Class type, Supplier unmodifiedSupplier) {
47 | if (HttpStatus.NOT_MODIFIED.equals(response.getStatusCode())) {
48 | return unmodifiedSupplier.get();
49 | }
50 |
51 | if (HttpStatus.NOT_FOUND.equals(response.getStatusCode())) {
52 | return null;
53 | }
54 |
55 | try {
56 | String json = response.getBody();
57 | if (json.startsWith("\uFEFF")) {
58 | json = json.substring(1);
59 | }
60 |
61 | if (json.startsWith("\u00EF\u00BB\u00BF")) {
62 | json = json.substring(3);
63 | }
64 |
65 | return JsonUtils.deserialize(json, type);
66 | } catch (IOException e) {
67 | throw new SourceException("Could not deserialize: " + e.getMessage(), e);
68 | }
69 | }
70 |
71 | public void downloadAsset(String url, Path assetPath, ProgressListeners progressListeners) {
72 | progressListeners.stepStarted(url, StepType.DOWNLOAD);
73 |
74 | String redirectURL = url;
75 | while (redirectURL != null) {
76 | redirectURL = this.restTemplate.execute(redirectURL, HttpMethod.GET, this::prepareRequest,
77 | new DownloadResponseExtractor(assetPath, progressListeners));
78 | }
79 | }
80 |
81 | public ResponseEntity fetch(String url, String etag) {
82 | if (this.isRateLimitReached()) {
83 | throw new RateLimitException(this.rateLimitReset);
84 | }
85 |
86 | try {
87 | ResponseEntity responseEntity = this.restTemplate.execute(url, HttpMethod.GET, new GZipRequestCallback(etag),
88 | new GzipResponseExtractor());
89 |
90 | this.handleRateLimit(responseEntity.getHeaders());
91 | return responseEntity;
92 | } catch (HttpClientErrorException e) {
93 | this.handleRateLimit(e.getResponseHeaders());
94 | return ResponseEntity.status(e.getRawStatusCode()).headers(e.getResponseHeaders()).body(e.getStatusText());
95 | } catch (NestedRuntimeException e) {
96 | Throwable mostSpecificCause = e.getMostSpecificCause();
97 | if (mostSpecificCause instanceof UnknownHostException) {
98 | throw new HostUnreachableException(mostSpecificCause.getMessage());
99 | }
100 | throw new RestClientException("Could not fetch from " + url + ": " + mostSpecificCause.getMessage(), mostSpecificCause);
101 | } catch (Exception e) {
102 | throw new RestClientException("Could not fetch from " + url + ": " + e.getMessage(), e);
103 | }
104 | }
105 |
106 | private void handleRateLimit(HttpHeaders headers) {
107 | String remaining = headers.getFirst("X-RateLimit-Remaining");
108 | if (StringUtils.isEmpty(remaining) || !remaining.equals("0")) {
109 | return;
110 | }
111 |
112 | String reset = headers.getFirst("X-RateLimit-Reset");
113 | if (StringUtils.isEmpty(reset)) {
114 | return;
115 | }
116 |
117 | this.rateLimitReset = Instant.ofEpochSecond(Long.parseLong(reset));
118 | }
119 |
120 | private boolean isRateLimitReached() {
121 | if (this.rateLimitReset == null) {
122 | return false;
123 | }
124 |
125 | if (this.rateLimitReset.isBefore(Instant.now())) {
126 | this.rateLimitReset = null;
127 | return false;
128 | }
129 |
130 | return true;
131 | }
132 |
133 | private void prepareRequest(@SuppressWarnings("unused") ClientHttpRequest request) {
134 | // nothing to do
135 | }
136 |
137 | protected static class GZipRequestCallback implements RequestCallback {
138 |
139 | private final String etag;
140 |
141 | public GZipRequestCallback(String etag) {
142 | super();
143 | this.etag = etag;
144 | }
145 |
146 | @Override
147 | public void doWithRequest(ClientHttpRequest request) throws IOException {
148 | request.getHeaders().setIfNoneMatch(this.etag);
149 | request.getHeaders().add("Accept-Encoding", "application/gzip");
150 | }
151 | }
152 |
153 | protected static class GzipResponseExtractor implements ResponseExtractor> {
154 |
155 | private static Charset getCharset(ClientHttpResponse response) {
156 | try {
157 | String charsetName = response.getHeaders().getContentType().getParameter("charset");
158 | if (charsetName != null) {
159 | return Charset.forName(charsetName);
160 | }
161 | } catch (Exception e) {
162 | // ignore
163 | }
164 |
165 | return StandardCharsets.ISO_8859_1;
166 | }
167 |
168 | @Override
169 | public ResponseEntity extractData(ClientHttpResponse response) throws IOException {
170 | InputStream inputStream = response.getBody();
171 | if ("gzip".equalsIgnoreCase(response.getHeaders().getFirst("Content-Encoding"))) {
172 | inputStream = new GZIPInputStream(inputStream);
173 | }
174 |
175 | Charset charset = getCharset(response);
176 | String body = StreamUtils.copyToString(inputStream, charset);
177 |
178 | return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(body);
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/rest/RestClientException.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.rest;
2 |
3 | import me.wulfmarius.modinstaller.ModInstallerException;
4 |
5 | public class RestClientException extends ModInstallerException {
6 |
7 | private static final long serialVersionUID = 1L;
8 |
9 | public RestClientException(String message) {
10 | super(message);
11 | }
12 |
13 | public RestClientException(String message, Throwable cause) {
14 | super(message, cause);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/BindingsFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import java.util.Optional;
4 | import java.util.function.Function;
5 |
6 | import javafx.beans.binding.Bindings;
7 | import javafx.beans.property.Property;
8 | import javafx.beans.value.ObservableValue;
9 | import me.wulfmarius.modinstaller.*;
10 |
11 | public class BindingsFactory {
12 |
13 | public static ObservableValue createHasValueBinding(Property> property) {
14 | return Bindings.createBooleanBinding(() -> property.getValue() != null, property);
15 | }
16 |
17 | public static ObservableValue createIsNotEmptyBinding(Property property) {
18 | return Bindings.createBooleanBinding(() -> property.getValue() != null && !property.getValue().isEmpty(), property);
19 | }
20 |
21 | public static ObservableValue createModDefinitionDescriptionBinding(Property property) {
22 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getDescription, null), property);
23 | }
24 |
25 | public static ObservableValue createModDefinitionInstallBinding(Property property,
26 | ModInstaller modInstaller) {
27 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isNoVersionInstalled, false), property);
28 | }
29 |
30 | public static ObservableValue createModDefinitionNameBinding(Property property) {
31 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getName, null), property);
32 | }
33 |
34 | public static ObservableValue createModDefinitionUninstallBinding(Property property,
35 | ModInstaller modInstaller) {
36 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isAnyVersionInstalled, false), property);
37 | }
38 |
39 | public static ObservableValue createModDefinitionUpdateBinding(Property property,
40 | ModInstaller modInstaller) {
41 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isOlderVersionInstalled, false), property);
42 | }
43 |
44 | public static ObservableValue createModDefinitionURLBinding(Property property) {
45 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getUrl, null), property);
46 | }
47 |
48 | public static ObservableValue createModDefinitionVersionBinding(Property property) {
49 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getVersion, null), property);
50 | }
51 |
52 | private static V get(Property property, Function function, V defaultValue) {
53 | return Optional.ofNullable(property.getValue()).map(function).orElse(defaultValue);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ChangeLogViewer.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ChangeLogViewerController.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import static me.wulfmarius.modinstaller.ui.ModInstallerUI.formatReleaseDate;
4 |
5 | import java.text.MessageFormat;
6 | import java.util.List;
7 | import java.util.concurrent.Callable;
8 |
9 | import javafx.application.Platform;
10 | import javafx.beans.binding.Bindings;
11 | import javafx.fxml.FXML;
12 | import javafx.scene.Node;
13 | import javafx.scene.control.*;
14 | import javafx.scene.layout.Pane;
15 | import me.wulfmarius.modinstaller.*;
16 |
17 | public class ChangeLogViewerController {
18 |
19 | @FXML
20 | private Pane pane;
21 |
22 | private ModInstaller modInstaller;
23 |
24 | public ChangeLogViewerController(ModInstaller modInstaller) {
25 | super();
26 | this.modInstaller = modInstaller;
27 | }
28 |
29 | public void setModDefinition(ModDefinition modDefinition) {
30 | List modDefinitions = this.modInstaller.getModDefinitions(modDefinition.getName());
31 |
32 | for (ModDefinition eachModDefinition : modDefinitions) {
33 | TextArea textArea = new TextArea(eachModDefinition.getChanges());
34 | textArea.setEditable(false);
35 | textArea.setWrapText(true);
36 |
37 | String title = MessageFormat.format("{0} - {1}", eachModDefinition.getVersion(),
38 | formatReleaseDate(eachModDefinition.getReleaseDate()));
39 | TitledPane titledPane = new TitledPane(title, textArea);
40 | titledPane.setExpanded(this.pane.getChildren().isEmpty());
41 | this.pane.getChildren().add(titledPane);
42 |
43 | Platform.runLater(() -> this.adjustHeight(textArea));
44 | }
45 | }
46 |
47 | private void adjustHeight(TextArea textArea) {
48 | Node text = textArea.lookup(".text");
49 | if (text == null) {
50 | return;
51 | }
52 |
53 | textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(new Callable() {
54 |
55 | @Override
56 | public Double call() throws Exception {
57 | return text.getBoundsInLocal().getHeight() + 10;
58 | }
59 | }, text.boundsInLocalProperty()));
60 | }
61 |
62 | @FXML
63 | private void onClose() {
64 | this.pane.getScene().getWindow().hide();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ControllerFactory.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import java.lang.reflect.Constructor;
4 | import java.net.URISyntaxException;
5 | import java.nio.file.*;
6 |
7 | import javafx.util.Callback;
8 | import me.wulfmarius.modinstaller.ModInstaller;
9 |
10 | public class ControllerFactory implements Callback, Object> {
11 |
12 | public static final ControllerFactory CONTROLLER_FACTORY = new ControllerFactory();
13 |
14 | private final ModInstaller modInstaller;
15 | private final Path baseDirectory;
16 |
17 | private ControllerFactory() {
18 | try {
19 | this.baseDirectory = Paths.get(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
20 | this.modInstaller = new ModInstaller(this.baseDirectory.resolve("mod-installer"));
21 | } catch (URISyntaxException e) {
22 | throw new IllegalStateException("Could not determine current baseDirectory.");
23 | }
24 | }
25 |
26 | public static Path getBaseDirectory() {
27 | return CONTROLLER_FACTORY.baseDirectory;
28 | }
29 |
30 | public static boolean isInstallationDirectoryValid() {
31 | if (Files.exists(ControllerFactory.getBaseDirectory().resolve("TLD.exe"))) {
32 | return true;
33 | }
34 |
35 | if (Files.exists(ControllerFactory.getBaseDirectory().resolve("tld.app"))) {
36 | return true;
37 | }
38 |
39 | if (Files.exists(ControllerFactory.getBaseDirectory().resolve("tld.x86"))) {
40 | return true;
41 | }
42 |
43 | if (Files.exists(ControllerFactory.getBaseDirectory().resolve("tld.x86_64"))) {
44 | return true;
45 | }
46 |
47 | return false;
48 |
49 | }
50 |
51 | @Override
52 | public Object call(Class> controllerClass) {
53 | try {
54 | for (Constructor> eachConstructor : controllerClass.getConstructors()) {
55 | if (eachConstructor.getParameterCount() == 1 && eachConstructor.getParameterTypes()[0] == ModInstaller.class) {
56 | return eachConstructor.newInstance(this.modInstaller);
57 | }
58 | }
59 |
60 | return controllerClass.newInstance();
61 | } catch (Exception e) {
62 | throw new RuntimeException(e);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/InstallerMainPanel.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
70 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ModDetailsPanel.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
100 |
105 |
117 |
122 |
134 |
135 |
147 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ModDetailsPanelController.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import static me.wulfmarius.modinstaller.ui.BindingsFactory.*;
4 | import static me.wulfmarius.modinstaller.ui.ModInstallerUI.startProgressDialog;
5 |
6 | import java.util.stream.Collectors;
7 |
8 | import javafx.beans.binding.Bindings;
9 | import javafx.beans.property.*;
10 | import javafx.fxml.FXML;
11 | import javafx.scene.Node;
12 | import javafx.scene.control.*;
13 | import javafx.scene.layout.AnchorPane;
14 | import me.wulfmarius.modinstaller.*;
15 | import me.wulfmarius.modinstaller.compatibility.CompatibilityChecker.Compatibility;
16 |
17 | public class ModDetailsPanelController {
18 |
19 | private final ModInstaller modInstaller;
20 |
21 | @FXML
22 | private AnchorPane anchorPane;
23 |
24 | @FXML
25 | private Label labelName;
26 | @FXML
27 | private Label labelVersion;
28 | @FXML
29 | private Hyperlink hyperlinkChangelog;
30 | @FXML
31 | private Hyperlink hyperlinkURL;
32 | @FXML
33 | private Label labelDescription;
34 |
35 | @FXML
36 | private Node panelRequires;
37 | @FXML
38 | private Label labelRequires;
39 |
40 | @FXML
41 | private Node panelRequiredBy;
42 | @FXML
43 | private Label labelRequiredBy;
44 |
45 | @FXML
46 | private Label infoInstallForbidden;
47 | @FXML
48 | private Button buttonInstall;
49 |
50 | @FXML
51 | private Label infoUpdateForbidden;
52 | @FXML
53 | private Button buttonUpdate;
54 |
55 | @FXML
56 | private Label infoUninstallForbidden;
57 | @FXML
58 | private Button buttonUninstall;
59 |
60 | @FXML
61 | private Label infoCompatibilityOld;
62 | @FXML
63 | private Label infoCompatibilityUnknown;
64 |
65 | private final RefreshableObjectProperty modDefinitionProperty = new RefreshableObjectProperty<>(null);
66 | private final Property requiresProperty = new RefreshableObjectProperty<>(null);
67 | private final Property requiredByProperty = new RefreshableObjectProperty<>(null);
68 |
69 | private final BooleanProperty compatibilityOldProperty = new SimpleBooleanProperty(false);
70 | private final BooleanProperty compatibilityUnknownProperty = new SimpleBooleanProperty(false);
71 |
72 | private final BooleanProperty installForbiddenProperty = new SimpleBooleanProperty(false);
73 | private final Property installTooltipProperty = new SimpleObjectProperty<>();
74 |
75 | private final BooleanProperty uninstallForbiddenProperty = new SimpleBooleanProperty(false);
76 | private final Property uninstallTooltipProperty = new SimpleObjectProperty<>();
77 |
78 | public ModDetailsPanelController(ModInstaller modInstaller) {
79 | super();
80 | this.modInstaller = modInstaller;
81 | }
82 |
83 | @FXML
84 | private void initialize() {
85 | this.modInstaller.addInstallationsChangedListener(this::installationsChanged);
86 |
87 | this.anchorPane.addEventHandler(ModInstallerEvent.MOD_SELECTED, this::onModSelected);
88 | this.anchorPane.visibleProperty().bind(createHasValueBinding(this.modDefinitionProperty));
89 |
90 | this.labelName.textProperty().bind(createModDefinitionNameBinding(this.modDefinitionProperty));
91 | this.labelVersion.textProperty().bind(createModDefinitionVersionBinding(this.modDefinitionProperty));
92 | this.hyperlinkChangelog.visitedProperty().bind(new SimpleBooleanProperty(false));
93 | this.hyperlinkURL.textProperty().bind(createModDefinitionURLBinding(this.modDefinitionProperty));
94 | this.hyperlinkURL.visitedProperty().bind(new SimpleBooleanProperty(false));
95 | this.labelDescription.textProperty().bind(createModDefinitionDescriptionBinding(this.modDefinitionProperty));
96 |
97 | this.panelRequires.visibleProperty().bind(createIsNotEmptyBinding(this.requiresProperty));
98 | this.panelRequires.managedProperty().bind(this.panelRequires.visibleProperty());
99 | this.labelRequires.textProperty().bind(this.requiresProperty);
100 |
101 | this.panelRequiredBy.visibleProperty().bind(createIsNotEmptyBinding(this.requiredByProperty));
102 | this.panelRequiredBy.managedProperty().bind(this.panelRequiredBy.visibleProperty());
103 | this.labelRequiredBy.textProperty().bind(this.requiredByProperty);
104 |
105 | this.buttonInstall.visibleProperty().bind(createModDefinitionInstallBinding(this.modDefinitionProperty, this.modInstaller));
106 | this.buttonInstall.disableProperty().bind(this.installForbiddenProperty);
107 | this.infoInstallForbidden.visibleProperty().bind(Bindings.and(this.buttonInstall.visibleProperty(), this.installForbiddenProperty));
108 | this.infoInstallForbidden.tooltipProperty().bind(this.installTooltipProperty);
109 |
110 | this.buttonUpdate.visibleProperty().bind(createModDefinitionUpdateBinding(this.modDefinitionProperty, this.modInstaller));
111 | this.buttonUpdate.disableProperty().bind(this.installForbiddenProperty);
112 | this.infoUpdateForbidden.visibleProperty().bind(Bindings.and(this.buttonUpdate.visibleProperty(), this.installForbiddenProperty));
113 | this.infoUpdateForbidden.tooltipProperty().bind(this.installTooltipProperty);
114 |
115 | this.buttonUninstall.visibleProperty().bind(createModDefinitionUninstallBinding(this.modDefinitionProperty, this.modInstaller));
116 | this.buttonUninstall.disableProperty().bind(this.uninstallForbiddenProperty);
117 | this.infoUninstallForbidden.visibleProperty().bind(this.uninstallForbiddenProperty);
118 | this.infoUninstallForbidden.tooltipProperty().bind(this.uninstallTooltipProperty);
119 |
120 | this.infoCompatibilityOld.visibleProperty().bind(this.compatibilityOldProperty);
121 | this.infoCompatibilityUnknown.visibleProperty().bind(this.compatibilityUnknownProperty);
122 | }
123 |
124 | private void installationsChanged() {
125 | this.modDefinitionProperty.fireValueChangedEvent();
126 | }
127 |
128 | private void installMod(ModDefinition modDefinition) {
129 | startProgressDialog("Installing " + modDefinition.getDisplayName(),
130 | this.anchorPane,
131 | () -> this.modInstaller.install(modDefinition));
132 | }
133 |
134 | @FXML
135 | private void onInstallMod() {
136 | ModDefinition modDefinition = this.modDefinitionProperty.getValue();
137 | if (modDefinition == null) {
138 | return;
139 | }
140 |
141 | if (this.compatibilityOldProperty.get()) {
142 | ModInstallerUI.showYesNoChoice("Incompatible Mod",
143 | "This mod is probably not compatible with your current version of The Long Dark and may cause problems.\nDo you want to install it anyway?",
144 | () -> this.installMod(modDefinition));
145 | return;
146 | }
147 |
148 | if (this.compatibilityUnknownProperty.get()) {
149 | ModInstallerUI.showYesNoChoice("Unknown Compatibility",
150 | "The compatibility of this mod could not be checked.\nDo you want to install it anyway?",
151 | () -> this.installMod(modDefinition));
152 | return;
153 | }
154 |
155 | this.installMod(modDefinition);
156 | }
157 |
158 | private void onModSelected(ModInstallerEvent event) {
159 | ModDefinition modDefinition = event.getModDefinition();
160 | this.modDefinitionProperty.setValue(modDefinition);
161 |
162 | if (modDefinition == null) {
163 | return;
164 | }
165 |
166 | this.updateDependencyResolution(modDefinition);
167 |
168 | this.requiresProperty.setValue(modDefinition.getDependenciesStream().map(ModDependency::getName).collect(Collectors.joining(", ")));
169 |
170 | ModDefinitions requiredBy = this.modInstaller.getRequiredBy(modDefinition);
171 | this.requiredByProperty.setValue(requiredBy.stream().map(ModDefinition::getName).collect(Collectors.joining(", ")));
172 |
173 | ModDefinitions installedRequiredBy = requiredBy.stream().filter(this.modInstaller::isAnyVersionInstalled).collect(
174 | ModDefinitions.toModDefinitions());
175 | this.uninstallForbiddenProperty.setValue(!installedRequiredBy.isEmpty());
176 | this.uninstallTooltipProperty.setValue(new Tooltip(installedRequiredBy.stream().map(ModDefinition::getName).collect(
177 | Collectors.joining(", ", "Required by installed mods: ", ""))));
178 |
179 | Compatibility compatibility = this.modInstaller.getCompatibility(modDefinition);
180 | this.compatibilityOldProperty.setValue(compatibility == Compatibility.OLD);
181 | this.compatibilityUnknownProperty.setValue(compatibility == Compatibility.UNKNOWN);
182 | }
183 |
184 | @FXML
185 | private void onUninstallMod() {
186 | ModDefinition modDefinition = this.modDefinitionProperty.getValue();
187 | if (modDefinition == null) {
188 | return;
189 | }
190 |
191 | startProgressDialog("Uninstalling " + modDefinition.getDisplayName(),
192 | this.anchorPane,
193 | () -> this.modInstaller.uninstallAll(modDefinition.getName()));
194 | }
195 |
196 | @FXML
197 | private void openURL() {
198 | ModInstallerUI.openURL(this.modDefinitionProperty.get().getUrl());
199 | }
200 |
201 | @FXML
202 | private void showChangelog() {
203 | ModInstallerUI.startChangeLogViewer(this.anchorPane, this.modDefinitionProperty.get());
204 | }
205 |
206 | private void updateDependencyResolution(ModDefinition modDefinition) {
207 | Resolution resolution = this.modInstaller.resolveInstallation(modDefinition);
208 | if (resolution.hasMissingDependencies()) {
209 | String message = resolution.getMissingDependencies().stream().map(ModDependency::getDisplayName).collect(
210 | Collectors.joining(", ", "Requires missing dependencies: ", ""));
211 | this.requiresProperty.setValue(message);
212 | this.installForbiddenProperty.set(true);
213 | this.installTooltipProperty.setValue(new Tooltip(message));
214 | } else if (resolution.hasUnresolvableDependencies()) {
215 | String message = resolution.getUnresolvableDependencies().stream().map(ModDependency::getDisplayName).collect(
216 | Collectors.joining(", ", "Causes version conflict: ", ""));
217 | this.requiresProperty.setValue(message);
218 | this.installForbiddenProperty.set(true);
219 | this.installTooltipProperty.setValue(new Tooltip(message));
220 | } else {
221 | this.installForbiddenProperty.set(false);
222 | }
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ModInstallerEvent.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import javafx.event.*;
4 | import me.wulfmarius.modinstaller.ModDefinition;
5 |
6 | public class ModInstallerEvent extends Event {
7 |
8 | private static final long serialVersionUID = 1L;
9 |
10 | public static EventType MOD_INSTALLER = new EventType<>("MOD_INSTALLER");
11 | public static EventType MOD_SELECTED = new EventType<>(MOD_INSTALLER, "MOD_SELECTED");
12 |
13 | private ModDefinition modDefinition;
14 |
15 | private ModInstallerEvent(EventType extends Event> eventType) {
16 | super(eventType);
17 | }
18 |
19 | public static ModInstallerEvent modSelected(ModDefinition modDefinition) {
20 | return new ModInstallerEvent(MOD_SELECTED).setModDefinition(modDefinition);
21 | }
22 |
23 | public ModDefinition getModDefinition() {
24 | return this.modDefinition;
25 | }
26 |
27 | private ModInstallerEvent setModDefinition(ModDefinition modDefinition) {
28 | this.modDefinition = modDefinition;
29 | return this;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ModInstallerMain.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import javafx.application.Application;
4 |
5 | public class ModInstallerMain {
6 |
7 | public static void main(String[] args) {
8 | Application.launch(ModInstallerUI.class, args);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ModInstallerUI.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import static me.wulfmarius.modinstaller.ui.ControllerFactory.CONTROLLER_FACTORY;
4 |
5 | import java.io.IOException;
6 | import java.net.URISyntaxException;
7 | import java.text.DateFormat;
8 | import java.util.*;
9 |
10 | import javafx.application.*;
11 | import javafx.fxml.FXMLLoader;
12 | import javafx.scene.*;
13 | import javafx.scene.control.*;
14 | import javafx.scene.control.Alert.AlertType;
15 | import javafx.scene.image.Image;
16 | import javafx.stage.*;
17 | import me.wulfmarius.modinstaller.*;
18 |
19 | public class ModInstallerUI extends Application {
20 |
21 | private static Image ICON = new Image("/icon.png");
22 |
23 | private static HostServices hostServices;
24 | private static Stage mainStage;
25 |
26 | public static void setTitle(String title) {
27 | mainStage.setTitle(title);
28 | }
29 |
30 | protected static String formatReleaseDate(Date date) {
31 | return DateFormat.getDateInstance(DateFormat.SHORT).format(date);
32 | }
33 |
34 | protected static void openURL(String url) {
35 | try {
36 | hostServices.showDocument(url);
37 | } catch (Exception e) {
38 | showError("Could Not Open", "Could not open URL '" + url + "': " + e.getMessage());
39 | }
40 | }
41 |
42 | protected static void showError(String title, String message) {
43 | if (!Platform.isFxApplicationThread()) {
44 | Platform.runLater(() -> showError(title, message));
45 | return;
46 | }
47 |
48 | Alert dialog = new Alert(AlertType.ERROR);
49 | dialog.setTitle(title);
50 | dialog.setHeaderText(null);
51 | dialog.getDialogPane().getStylesheets().add(ModInstaller.class.getResource("/global.css").toExternalForm());
52 | dialog.setContentText(message);
53 | ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().add(ICON);
54 | dialog.showAndWait();
55 | }
56 |
57 | protected static Optional showYesNoChoice(String title, String message) {
58 | Alert dialog = new Alert(AlertType.CONFIRMATION, message, ButtonType.YES, ButtonType.NO);
59 | dialog.setTitle(title);
60 | dialog.setHeaderText(null);
61 | ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().add(ICON);
62 | return dialog.showAndWait();
63 | }
64 |
65 | protected static void showYesNoChoice(String title, String message, Runnable onYes) {
66 | if (ModInstallerUI.showYesNoChoice(title, message).filter(ButtonType.YES::equals).isPresent()) {
67 | onYes.run();
68 | }
69 | }
70 |
71 | protected static void startAutoCloseProgressDialog(String title, Node ownerNode, Runnable runnable) {
72 | progressDialog(title, ownerNode, runnable, null, true);
73 | }
74 |
75 | protected static void startAutoCloseProgressDialog(String title, Node ownerNode, Runnable runnable, Runnable onClosed) {
76 | progressDialog(title, ownerNode, runnable, onClosed, true);
77 | }
78 |
79 | protected static void startChangeLogViewer(Node ownerNode, ModDefinition modDefinition) {
80 | try {
81 | FXMLLoader fxmlLoader = new FXMLLoader(ProgressDialogController.class.getResource("ChangeLogViewer.fxml"));
82 | fxmlLoader.setControllerFactory(CONTROLLER_FACTORY);
83 | fxmlLoader.load();
84 | ChangeLogViewerController controller = fxmlLoader.getController();
85 | controller.setModDefinition(modDefinition);
86 |
87 | Stage stage = new Stage();
88 | stage.setScene(new Scene(fxmlLoader.getRoot()));
89 | stage.setTitle("Change Log - " + modDefinition.getName());
90 | stage.getIcons().add(ICON);
91 | stage.initModality(Modality.APPLICATION_MODAL);
92 | stage.initOwner(ownerNode.getScene().getWindow());
93 | stage.setResizable(false);
94 | stage.centerOnScreen();
95 |
96 | stage.show();
97 | } catch (IOException e) {
98 | showError("Could not show change log viewer", e.getMessage());
99 | }
100 | }
101 |
102 | protected static void startProgressDialog(String title, Node ownerNode, Runnable runnable) {
103 | progressDialog(title, ownerNode, runnable, null, false);
104 | }
105 |
106 | protected static void startProgressDialog(String title, Node ownerNode, Runnable runnable, Runnable onClosed) {
107 | progressDialog(title, ownerNode, runnable, onClosed, false);
108 | }
109 |
110 | private static void progressDialog(String title, Node ownerNode, Runnable runnable, Runnable onClosed,
111 | boolean autoCloseWithoutErrors) {
112 | if (!Platform.isFxApplicationThread()) {
113 | Platform.runLater(() -> progressDialog(title, ownerNode, runnable, onClosed, autoCloseWithoutErrors));
114 | return;
115 | }
116 |
117 | try {
118 | FXMLLoader fxmlLoader = new FXMLLoader(ProgressDialogController.class.getResource("ProgressDialog.fxml"));
119 | fxmlLoader.setControllerFactory(CONTROLLER_FACTORY);
120 | fxmlLoader.load();
121 | ProgressDialogController controller = fxmlLoader.getController();
122 | controller.setRunnable(runnable);
123 | controller.setAutoCloseWithoutErrors(autoCloseWithoutErrors);
124 |
125 | Stage stage = new Stage();
126 | stage.setScene(new Scene(fxmlLoader.getRoot()));
127 | stage.setTitle(title);
128 | stage.getIcons().add(ICON);
129 | stage.initModality(Modality.APPLICATION_MODAL);
130 | stage.initOwner(ownerNode.getScene().getWindow());
131 |
132 | if (onClosed != null) {
133 | stage.setOnHidden(event -> onClosed.run());
134 | }
135 |
136 | stage.show();
137 | } catch (IOException e) {
138 | showError("Could Not Show Dialog", e.getMessage());
139 | }
140 | }
141 |
142 | @Override
143 | public void start(Stage primaryStage) throws IOException, URISyntaxException {
144 | if (System.getProperty("SKIP_DIRECTORY_CHECK") == null && !ControllerFactory.isInstallationDirectoryValid()) {
145 | showError("Invalid Installation Directory",
146 | "Mod-Installer appears to be in the wrong directory.\n\n"
147 | + "Make sure you put it into the directory \"TheLongDark\", which contains the \"tld\" executable.\n\n"
148 | + "Working directory is " + ControllerFactory.getBaseDirectory());
149 | primaryStage.close();
150 | return;
151 | }
152 |
153 | hostServices = this.getHostServices();
154 | mainStage = primaryStage;
155 |
156 | FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("InstallerMainPanel.fxml"));
157 | fxmlLoader.setControllerFactory(ControllerFactory.CONTROLLER_FACTORY);
158 | Parent mainPanel = fxmlLoader.load();
159 |
160 | Scene scene = new Scene(mainPanel);
161 | scene.getStylesheets().add("global.css");
162 |
163 | primaryStage.setScene(scene);
164 | primaryStage.sizeToScene();
165 | primaryStage.setTitle("TLD Mod-Installer " + ModInstaller.VERSION);
166 | primaryStage.getIcons().add(ICON);
167 | primaryStage.show();
168 |
169 | primaryStage.setMinWidth(600);
170 | primaryStage.setMinHeight(350);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ProgressDialog.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/ProgressDialogController.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import static java.text.MessageFormat.format;
4 | import static me.wulfmarius.modinstaller.utils.StringUtils.*;
5 |
6 | import java.util.*;
7 |
8 | import javafx.application.Platform;
9 | import javafx.beans.property.*;
10 | import javafx.fxml.FXML;
11 | import javafx.scene.control.*;
12 | import javafx.scene.layout.Pane;
13 | import me.wulfmarius.modinstaller.*;
14 |
15 | public class ProgressDialogController implements ProgressListener {
16 |
17 | private final ModInstaller modInstaller;
18 |
19 | private String currentStep;
20 | private StepType currentStepType;
21 |
22 | @FXML
23 | private Pane pane;
24 |
25 | @FXML
26 | private Button buttonClose;
27 |
28 | @FXML
29 | private Label labelStep;
30 |
31 | @FXML
32 | private ProgressBar progressBarStep;
33 |
34 | @FXML
35 | private TextArea textAreaLog;
36 |
37 | @FXML
38 | private Label labelTime;
39 |
40 | private final Clock clock = new Clock();
41 |
42 | private boolean autoCloseWithoutErrors;
43 | private boolean hadError;
44 |
45 | public ProgressDialogController(ModInstaller modInstaller) {
46 | super();
47 | this.modInstaller = modInstaller;
48 | }
49 |
50 | @Override
51 | public void finished(String message) {
52 | if (!Platform.isFxApplicationThread()) {
53 | Platform.runLater(() -> this.finished(message));
54 | return;
55 | }
56 |
57 | this.appendToLog("FINISHED");
58 | if (message != null) {
59 | this.appendToLog(message);
60 | }
61 |
62 | this.progressBarStep.setProgress(1);
63 | this.buttonClose.setDisable(false);
64 | this.buttonClose.requestFocus();
65 | this.clock.stop();
66 |
67 | if (this.autoCloseWithoutErrors && !this.hadError) {
68 | this.onClose();
69 | }
70 | }
71 |
72 | public boolean isAutoCloseWithoutErrors() {
73 | return this.autoCloseWithoutErrors;
74 | }
75 |
76 | public void setAutoCloseWithoutErrors(boolean autoCloseWithoutErrors) {
77 | this.autoCloseWithoutErrors = autoCloseWithoutErrors;
78 | }
79 |
80 | public void setRunnable(Runnable runnable) {
81 | WindowBecameVisibleHandler.install(this.pane, runnable);
82 | }
83 |
84 | @Override
85 | public void started(String name) {
86 | if (!Platform.isFxApplicationThread()) {
87 | Platform.runLater(() -> this.started(name));
88 | return;
89 | }
90 |
91 | this.buttonClose.setDisable(true);
92 | this.buttonClose.getScene().getWindow().setOnCloseRequest(event -> {
93 | if (this.buttonClose.isDisabled()) {
94 | event.consume();
95 | }
96 | });
97 | }
98 |
99 | @Override
100 | public void stepDetail(String detail) {
101 | if (!Platform.isFxApplicationThread()) {
102 | Platform.runLater(() -> this.stepDetail(detail));
103 | return;
104 | }
105 |
106 | this.appendToLog("\t" + detail);
107 | }
108 |
109 | @Override
110 | public void stepError(String error) {
111 | if (!Platform.isFxApplicationThread()) {
112 | Platform.runLater(() -> this.stepError(error));
113 | return;
114 | }
115 |
116 | this.appendToLog("ERROR: " + error);
117 | this.hadError = true;
118 | }
119 |
120 | @Override
121 | public void stepProgress(int completed, int total) {
122 | if (!Platform.isFxApplicationThread()) {
123 | Platform.runLater(() -> this.stepProgress(completed, total));
124 | return;
125 | }
126 |
127 | this.progressBarStep.setProgress((double) completed / total);
128 | if (this.currentStepType == StepType.DOWNLOAD) {
129 | this.labelStep.setText(this.currentStepType + " " + shortenPath(this.currentStep) + ": " + formatByteCount(completed) + "/"
130 | + formatByteCount(total));
131 | }
132 | }
133 |
134 | @Override
135 | public void stepStarted(String step, StepType stepType) {
136 | if (!Platform.isFxApplicationThread()) {
137 | Platform.runLater(() -> this.stepStarted(step, stepType));
138 | return;
139 | }
140 |
141 | this.currentStep = step;
142 | this.currentStepType = stepType;
143 | this.labelStep.setText(stepType + ": " + step);
144 | this.appendToLog(stepType + ": " + step);
145 |
146 | this.clock.start();
147 | }
148 |
149 | private void appendToLog(String message) {
150 | this.textAreaLog.appendText(message + "\n");
151 | this.textAreaLog.setScrollTop(Double.MAX_VALUE);
152 | }
153 |
154 | @FXML
155 | private void initialize() {
156 | this.modInstaller.addProgressListener(this);
157 | this.labelTime.textProperty().bind(this.clock.formattedTime);
158 | }
159 |
160 | @FXML
161 | private void onClose() {
162 | this.modInstaller.removeProgressListener(this);
163 | this.buttonClose.getScene().getWindow().hide();
164 | }
165 |
166 | protected static class Clock {
167 |
168 | protected final StringProperty formattedTime = new SimpleStringProperty();
169 |
170 | private final Timer timer = new Timer(true);
171 | private long startTime;
172 | private boolean running;
173 |
174 | public boolean isRunning() {
175 | return this.running;
176 | }
177 |
178 | public void reset() {
179 | this.startTime = System.currentTimeMillis();
180 | this.formattedTime.set("");
181 | }
182 |
183 | public void start() {
184 | if (this.running) {
185 | return;
186 | }
187 |
188 | Platform.runLater(this::reset);
189 |
190 | this.timer.scheduleAtFixedRate(new TimerTask() {
191 |
192 | @Override
193 | public void run() {
194 | if (!Clock.this.isRunning()) {
195 | this.cancel();
196 | return;
197 | }
198 |
199 | Platform.runLater(Clock.this::update);
200 | }
201 | }, 1000, 200);
202 | this.running = true;
203 | }
204 |
205 | public void stop() {
206 | this.running = false;
207 | }
208 |
209 | protected void update() {
210 | long elapsed = System.currentTimeMillis() - this.startTime;
211 | long minutes = elapsed / 60000;
212 | elapsed %= 60000;
213 | long seconds = elapsed / 1000;
214 | this.formattedTime.set(format("{0,number,00}:{1,number,00}", minutes, seconds));
215 | }
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/RefreshableObjectProperty.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import javafx.beans.property.SimpleObjectProperty;
4 |
5 | public class RefreshableObjectProperty extends SimpleObjectProperty {
6 |
7 | protected RefreshableObjectProperty(T initialValue) {
8 | super(initialValue);
9 | }
10 |
11 | @Override
12 | public void fireValueChangedEvent() {
13 | super.fireValueChangedEvent();
14 | }
15 | }
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/ui/WindowBecameVisibleHandler.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.ui;
2 |
3 | import javafx.beans.value.ObservableValue;
4 | import javafx.scene.*;
5 | import javafx.stage.*;
6 |
7 | public class WindowBecameVisibleHandler {
8 |
9 | private final Runnable runnable;
10 |
11 | private WindowBecameVisibleHandler(Runnable runnable) {
12 | super();
13 | this.runnable = runnable;
14 | }
15 |
16 | public static void install(Node node, Runnable runnable) {
17 | WindowBecameVisibleHandler windowBecameVisibleHandler = new WindowBecameVisibleHandler(runnable);
18 | node.sceneProperty().addListener(windowBecameVisibleHandler::onSceneChanged);
19 | }
20 |
21 | @SuppressWarnings("unused")
22 | private void onSceneChanged(ObservableValue extends Scene> observable, Scene oldScene, Scene newScene) {
23 | if (newScene == null) {
24 | return;
25 | }
26 |
27 | newScene.windowProperty().addListener(this::onWindowChanged);
28 | }
29 |
30 | @SuppressWarnings("unused")
31 | private void onWindowChanged(ObservableValue extends Window> observable, Window oldWindow, Window newWindow) {
32 | if (newWindow == null) {
33 | return;
34 | }
35 |
36 | newWindow.addEventHandler(WindowEvent.WINDOW_SHOWN, event -> new Thread(this.runnable).start());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/update/UpdateChecker.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.update;
2 |
3 | import java.io.IOException;
4 | import java.net.URISyntaxException;
5 | import java.nio.file.*;
6 | import java.time.Instant;
7 | import java.time.temporal.ChronoUnit;
8 | import java.util.Date;
9 | import java.util.stream.Stream;
10 |
11 | import org.springframework.http.ResponseEntity;
12 |
13 | import me.wulfmarius.modinstaller.*;
14 | import me.wulfmarius.modinstaller.repository.source.*;
15 | import me.wulfmarius.modinstaller.rest.RestClient;
16 | import me.wulfmarius.modinstaller.utils.JsonUtils;
17 |
18 | public class UpdateChecker {
19 |
20 | private static final String LATEST_RELEASE_URL = "https://api.github.com/repos/WulfMarius/Mod-Installer/releases/latest";
21 |
22 | private final Path basePath;
23 | private final RestClient restClient;
24 | private final UpdateState state;
25 |
26 | private Path downloadPath;
27 | private Path[] otherVersions;
28 |
29 | public UpdateChecker(Path basePath, RestClient restClient) {
30 | super();
31 |
32 | this.basePath = basePath;
33 | this.restClient = restClient;
34 | this.state = this.readState();
35 | }
36 |
37 | public boolean areOtherVersionsPresent() {
38 | return this.otherVersions != null && this.otherVersions.length > 0;
39 | }
40 |
41 | public void downloadNewVersion(Path directory, ProgressListeners progressListeners) {
42 | this.downloadPath = directory.resolve(this.getState().getAsset().getName());
43 | this.restClient.downloadAsset(this.state.getAsset().getDownloadUrl(), this.downloadPath, progressListeners);
44 | }
45 |
46 | public void findLatestVersion() {
47 | if (!this.canCheckForNewVersion()) {
48 | return;
49 | }
50 |
51 | ResponseEntity response = this.restClient.fetch(LATEST_RELEASE_URL, this.state.getEtag());
52 | if (response.getStatusCode().is2xxSuccessful()) {
53 | GithubRelease latestRelease = this.restClient.deserialize(response, GithubRelease.class, GithubRelease::new);
54 | this.state.setLatestVersion(latestRelease.getName());
55 |
56 | GithubAsset[] assets = latestRelease.getAssets();
57 | if (assets != null && assets.length == 1) {
58 | this.state.setAsset(assets[0]);
59 | } else {
60 | this.state.setAsset(null);
61 | }
62 | this.state.setReleaseUrl(latestRelease.getUrl());
63 | }
64 |
65 | this.state.setEtag(response.getHeaders().getETag());
66 | this.state.setChecked(new Date());
67 | }
68 |
69 | public void findOtherVersions() {
70 | Path currentJar = this.getCurrentJarPath();
71 |
72 | try (Stream stream = Files.list(this.basePath)) {
73 | this.otherVersions = stream.filter(Files::isRegularFile)
74 | .filter(file -> !file.getFileName().equals(currentJar))
75 | .filter(file -> file.getFileName().toString().startsWith("mod-installer-")
76 | && file.getFileName().toString().endsWith(".jar"))
77 | .toArray(Path[]::new);
78 | } catch (IOException e) {
79 | this.otherVersions = new Path[0];
80 | }
81 | }
82 |
83 | public String getDownloadURL() {
84 | return this.state.getReleaseUrl();
85 | }
86 |
87 | public String getLatestVersion() {
88 | return this.state.getLatestVersion();
89 | }
90 |
91 | public Path[] getOtherVersions() {
92 | return this.otherVersions;
93 | }
94 |
95 | public UpdateState getState() {
96 | return this.state;
97 | }
98 |
99 | public boolean hasDownloadedNewVersion() {
100 | return this.downloadPath != null && Files.exists(this.downloadPath);
101 | }
102 |
103 | public void initialize() {
104 | if (System.getProperty("SKIP_SELF_UPDATE_CHECK") != null) {
105 | this.state.setLatestVersion(null);
106 | return;
107 | }
108 |
109 | this.findLatestVersion();
110 | this.findOtherVersions();
111 |
112 | this.writeState();
113 | }
114 |
115 | public boolean isNewVersionAvailable(String version) {
116 | if (this.state.getLatestVersion() == null) {
117 | return false;
118 | }
119 |
120 | return Version.parse(this.getLatestVersion()).compareTo(Version.parse(version)) > 0;
121 | }
122 |
123 | public void startNewVersion() throws IOException {
124 | new ProcessBuilder("java", "-jar", this.downloadPath.getFileName().toString()).inheritIO().start();
125 | }
126 |
127 | private boolean canCheckForNewVersion() {
128 | if (this.state.getChecked() == null) {
129 | return true;
130 | }
131 |
132 | return this.state.getChecked().toInstant().plus(5, ChronoUnit.MINUTES).isBefore(Instant.now());
133 | }
134 |
135 | private Path getCurrentJarPath() {
136 | try {
137 | return Paths.get(UpdateChecker.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getFileName();
138 | } catch (URISyntaxException e) {
139 | return null;
140 | }
141 | }
142 |
143 | private Path getUpdateCheckStatePath() {
144 | return Paths.get(System.getProperty("java.io.tmpdir"), "tld-mod-installer-update-check-state.json");
145 | }
146 |
147 | private UpdateState readState() {
148 | try {
149 | Path path = this.getUpdateCheckStatePath();
150 | if (Files.exists(path)) {
151 | return JsonUtils.deserialize(path, UpdateState.class);
152 | }
153 | } catch (IOException e) {
154 | // ignore
155 | }
156 |
157 | return new UpdateState();
158 | }
159 |
160 | private void writeState() {
161 | try {
162 | Path path = this.getUpdateCheckStatePath();
163 | Files.createDirectories(path.getParent());
164 | JsonUtils.serialize(path, this.state);
165 | } catch (IOException e) {
166 | // ignore
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/update/UpdateState.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.update;
2 |
3 | import java.util.Date;
4 |
5 | import me.wulfmarius.modinstaller.repository.source.GithubAsset;
6 |
7 | public class UpdateState {
8 |
9 | private String latestVersion;
10 | private String etag;
11 | private Date checked;
12 | private String releaseUrl;
13 | private GithubAsset asset;
14 |
15 | public GithubAsset getAsset() {
16 | return this.asset;
17 | }
18 |
19 | public Date getChecked() {
20 | return this.checked;
21 | }
22 |
23 | public String getEtag() {
24 | return this.etag;
25 | }
26 |
27 | public String getLatestVersion() {
28 | return this.latestVersion;
29 | }
30 |
31 | public String getReleaseUrl() {
32 | return this.releaseUrl;
33 | }
34 |
35 | public boolean hasAsset() {
36 | return this.asset != null;
37 | }
38 |
39 | public void setAsset(GithubAsset asset) {
40 | this.asset = asset;
41 | }
42 |
43 | public void setChecked(Date checked) {
44 | this.checked = checked;
45 | }
46 |
47 | public void setEtag(String etag) {
48 | this.etag = etag;
49 | }
50 |
51 | public void setLatestVersion(String latestVersion) {
52 | this.latestVersion = latestVersion;
53 | }
54 |
55 | public void setReleaseUrl(String releaseUrl) {
56 | this.releaseUrl = releaseUrl;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/utils/JsonUtils.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.utils;
2 |
3 | import java.io.*;
4 | import java.nio.file.*;
5 |
6 | import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
7 | import com.fasterxml.jackson.annotation.JsonInclude.Include;
8 | import com.fasterxml.jackson.annotation.PropertyAccessor;
9 | import com.fasterxml.jackson.databind.*;
10 |
11 | public class JsonUtils {
12 |
13 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
14 |
15 | static {
16 | OBJECT_MAPPER.setSerializationInclusion(Include.NON_NULL);
17 | OBJECT_MAPPER.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
18 | OBJECT_MAPPER.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
19 | OBJECT_MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
20 | OBJECT_MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
21 | OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
22 | }
23 |
24 | public static T deserialize(InputStream inputStream, Class type) throws IOException {
25 | return OBJECT_MAPPER.readValue(inputStream, type);
26 | }
27 |
28 | public static T deserialize(Path path, Class type) throws IOException {
29 | try (InputStream inputStream = Files.newInputStream(path)) {
30 | return deserialize(inputStream, type);
31 | }
32 | }
33 |
34 | public static T deserialize(String content, Class type) throws IOException {
35 | return OBJECT_MAPPER.readValue(content, type);
36 | }
37 |
38 | public static void serialize(Path path, Object value) throws IOException {
39 | try (OutputStream outputStream = Files.newOutputStream(path)) {
40 | OBJECT_MAPPER.writeValue(outputStream, value);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/utils/OsUtils.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.utils;
2 |
3 |
4 | public class OsUtils {
5 |
6 | public static boolean isMac() {
7 | return System.getProperty("os.name").toLowerCase().contains("mac");
8 | }
9 |
10 | public static boolean isWindows() {
11 | return System.getProperty("os.name").toLowerCase().contains("win");
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/me/wulfmarius/modinstaller/utils/StringUtils.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller.utils;
2 |
3 | import java.text.MessageFormat;
4 |
5 | public class StringUtils {
6 |
7 | private static final int GI_BYTE = 1024 * 1024 * 1024;
8 | private static final int MI_BYTE = 1024 * 1024;
9 | private static final int KI_BYTE = 1024;
10 |
11 | public static String formatByteCount(long byteCount) {
12 | if (byteCount > GI_BYTE) {
13 | return MessageFormat.format("{0,number,0.00} GiB", (double) byteCount / GI_BYTE);
14 | }
15 |
16 | if (byteCount > MI_BYTE) {
17 | return MessageFormat.format("{0,number,0.0} MiB", (double) byteCount / MI_BYTE);
18 | }
19 |
20 | if (byteCount > KI_BYTE) {
21 | return MessageFormat.format("{0,number,0.0} KiB", (double) byteCount / KI_BYTE);
22 | }
23 |
24 | return MessageFormat.format("{0} B", byteCount);
25 | }
26 |
27 | public static String shortenPath(String path) {
28 | if (path.length() < 60) {
29 | return path;
30 | }
31 |
32 | int firstPartLength = 30;
33 | int lastPathLength = 30;
34 |
35 | return path.substring(0, firstPartLength) + "..." + path.substring(path.length() - lastPathLength, path.length());
36 | }
37 |
38 | public static String trimToEmpty(String value) {
39 | if (value == null) {
40 | return "";
41 | }
42 |
43 | return value.trim();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/resources/add_circle_outline_black_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/add_circle_outline_black_24x24.png
--------------------------------------------------------------------------------
/src/main/resources/arrow_upward_black_18x18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/arrow_upward_black_18x18.png
--------------------------------------------------------------------------------
/src/main/resources/baseline_access_time_red_18x18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/baseline_access_time_red_18x18.png
--------------------------------------------------------------------------------
/src/main/resources/baseline_access_time_red_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/baseline_access_time_red_24x24.png
--------------------------------------------------------------------------------
/src/main/resources/baseline_filter_list_black_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/baseline_filter_list_black_24x24.png
--------------------------------------------------------------------------------
/src/main/resources/default-compatibility-state.json:
--------------------------------------------------------------------------------
1 | {
2 | "compatibilityVersions" : {
3 | "versions" : [ {
4 | "version" : "v1.41",
5 | "date" : "2018-12-17T00:00:00.000+0000"
6 | }, {
7 | "version" : "v1.27",
8 | "date" : "2018-03-21T00:00:00.000+0000"
9 | } ]
10 | }
11 | }
--------------------------------------------------------------------------------
/src/main/resources/folder_open_black_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/folder_open_black_24.png
--------------------------------------------------------------------------------
/src/main/resources/global.css:
--------------------------------------------------------------------------------
1 | .alert > *.label.content {
2 | -fx-pref-width: 400px;
3 | -fx-wrap-text: true;
4 | }
5 |
6 | .combo-box .list-cell
7 | {
8 | -fx-padding: 3 0 3 3;
9 | }
10 |
11 |
12 | .uninstall.button .text {
13 | -fx-fill: RED;
14 | }
15 |
16 | .update.button .text {
17 | -fx-fill: GREEN;
18 | }
19 |
20 |
21 | .hyperlink:visited .text {
22 | }
23 |
24 |
25 | .table-view .align-right.column-header .label {
26 | -fx-alignment: CENTER_RIGHT;
27 | }
28 | .table-view .align-left.column-header .label {
29 | -fx-alignment: CENTER_LEFT;
30 | }
31 |
32 |
33 | .table-row-cell:not-installed .text {
34 | -fx-opacity: 0.5;
35 | }
36 |
37 |
38 | .table-row-cell .tooltip .text {
39 | -fx-fill: WHITE;
40 | -fx-opacity: 1;
41 | }
42 |
43 | .table-cell {
44 | -fx-background-repeat: no-repeat;
45 | }
46 |
47 | .table-cell:required {
48 | -fx-background-image: url("/lock_grey_18x18.png");
49 | -fx-background-position: right;
50 | }
51 |
52 | .table-cell:update-available {
53 | -fx-background-image: url("/arrow_upward_black_18x18.png");
54 | -fx-background-position: right;
55 | }
56 |
57 | .table-cell:recent {
58 | -fx-background-image: url("new_releases_grey_18x18.png");
59 | -fx-background-position: right;
60 | }
61 |
62 | .table-cell:incompatible {
63 | -fx-background-image: url("baseline_access_time_red_18x18.png");
64 | -fx-background-position: right;
65 | }
66 |
67 | .table-cell:required:update-available {
68 | -fx-background-image: url("/arrow_upward_black_18x18.png"), url("/lock_grey_18x18.png");
69 | -fx-background-position: right 18.0px center, right;
70 | }
71 |
72 | .table-cell:required:incompatible {
73 | -fx-background-image: url("/lock_grey_18x18.png"), url("baseline_access_time_red_18x18.png");
74 | -fx-background-position: right 18.0px center, right;
75 | }
76 |
77 | .table-cell:required:update-available:incompatible {
78 | -fx-background-image: url("/arrow_upward_black_18x18.png"), url("/lock_grey_18x18.png"), url("baseline_access_time_red_18x18.png");
79 | -fx-background-position: right 36.0px center, right 18.0px center, right;
80 | }
81 |
82 | .table-cell:update-available:incompatible {
83 | -fx-background-image: url("/arrow_upward_black_18x18.png"), url("baseline_access_time_red_18x18.png");
84 | -fx-background-position: right 18.0px center, right;
85 | }
86 | .table-cell:recent:incompatible {
87 | -fx-background-image: url("new_releases_grey_18x18.png"), url("baseline_access_time_red_18x18.png");
88 | -fx-background-position: right 18.0px center, right;
89 | }
--------------------------------------------------------------------------------
/src/main/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/icon.png
--------------------------------------------------------------------------------
/src/main/resources/lock_grey_18x18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/lock_grey_18x18.png
--------------------------------------------------------------------------------
/src/main/resources/lock_grey_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/lock_grey_24x24.png
--------------------------------------------------------------------------------
/src/main/resources/new_releases_grey_18x18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/new_releases_grey_18x18.png
--------------------------------------------------------------------------------
/src/main/resources/refresh_black_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/src/main/resources/refresh_black_24x24.png
--------------------------------------------------------------------------------
/src/test/java/me/wulfmarius/modinstaller/DependencyResolverTest.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import java.io.IOException;
6 | import java.nio.file.*;
7 | import java.util.Arrays;
8 |
9 | import org.junit.*;
10 | import org.springframework.util.FileSystemUtils;
11 |
12 | import me.wulfmarius.modinstaller.repository.*;
13 |
14 | public class DependencyResolverTest {
15 |
16 | private DependencyResolver resolver;
17 | private Repository repository;
18 | private Installations installations;
19 |
20 | private static void assertMatches(Installations actual, ModDefinition... expected) {
21 | assertEquals(expected.length, actual.getSize());
22 |
23 | for (ModDefinition eachExpected : expected) {
24 | assertTrue(actual.contains(eachExpected));
25 | }
26 | }
27 |
28 | private static void assertMatches(ModDefinitions actual, ModDefinition... expected) {
29 | assertEquals("Expected: " + Arrays.toString(expected) + ", Actual: " + actual, expected.length, actual.getSize());
30 |
31 | for (ModDefinition eachExpected : expected) {
32 | assertTrue(actual.contains(eachExpected));
33 | }
34 | }
35 |
36 | private static void assertMatches(ModDependencies actual, ModDependency... expected) {
37 | assertEquals(expected.length, actual.getSize());
38 |
39 | for (ModDependency eachExpected : expected) {
40 | assertTrue(actual.contains(eachExpected));
41 | }
42 | }
43 |
44 | private static ModDependency createDependency(String name, String version) {
45 | ModDependency result = new ModDependency();
46 |
47 | result.setName(name);
48 | result.setVersion(version);
49 |
50 | return result;
51 | }
52 |
53 | private static Installation createInstallation(ModDefinition modDefinition) {
54 | Installation installation = new Installation();
55 |
56 | installation.setName(modDefinition.getName());
57 | installation.setVersion(modDefinition.getVersion());
58 |
59 | return installation;
60 | }
61 |
62 | @Before
63 | public void before() throws IOException {
64 | Path path = Paths.get("./target/resolver-test");
65 | FileSystemUtils.deleteRecursively(path);
66 |
67 | this.repository = new Repository(path);
68 | this.repository.initialize();
69 | this.repository.registerSource("./src/test/resources/mod-a.json");
70 | this.repository.registerSource("./src/test/resources/mod-b.json");
71 | this.repository.registerSource("./src/test/resources/mod-c.json");
72 | this.repository.registerSource("./src/test/resources/mod-d.json");
73 | this.repository.registerSource("./src/test/resources/mod-e.json");
74 |
75 | this.installations = new Installations();
76 |
77 | this.resolver = new DependencyResolver(this.repository, this.installations);
78 | }
79 |
80 | @Test
81 | public void install() {
82 | ModDefinition a10 = this.repository.getModDefinition("A", "1.0.0").get();
83 |
84 | Resolution resolution = this.resolver.resolve(a10);
85 |
86 | assertFalse(resolution.isErroneous());
87 | assertMatches(resolution.getInstall(), a10);
88 | assertMatches(resolution.getUninstall());
89 | }
90 |
91 | @Test
92 | public void installWithConflict() {
93 | ModDefinition a12 = this.repository.getModDefinition("A", "1.2.0").get();
94 | ModDefinition b11 = this.repository.getModDefinition("B", "1.1.0").get();
95 | ModDefinition c10 = this.repository.getModDefinition("C", "1.0.0").get();
96 |
97 | this.installations.addInstallation(createInstallation(a12));
98 | this.installations.addInstallation(createInstallation(c10));
99 |
100 | Resolution resolution = this.resolver.resolve(b11);
101 |
102 | assertTrue(resolution.isErroneous());
103 | assertFalse(resolution.hasMissingDependencies());
104 | assertTrue(resolution.hasUnresolvableDependencies());
105 |
106 | assertMatches(resolution.getUnresolvableDependencies(), createDependency("C", "1.0.0"), createDependency("C", "^1.1.0"));
107 | }
108 |
109 | @Test
110 | public void installWithDependencies() {
111 | ModDefinition a12 = this.repository.getModDefinition("A", "1.2.0").get();
112 | ModDefinition c1 = this.repository.getModDefinition("C", "1.0.0").get();
113 |
114 | Resolution resolution = this.resolver.resolve(a12);
115 |
116 | assertFalse(resolution.isErroneous());
117 | assertMatches(resolution.getInstall(), a12, c1);
118 | assertMatches(resolution.getUninstall());
119 | }
120 |
121 | @Test
122 | public void installWithExistingDependencies() {
123 | ModDefinition a13 = this.repository.getModDefinition("A", "1.3.0").get();
124 | ModDefinition b11 = this.repository.getModDefinition("B", "1.1.0").get();
125 | ModDefinition c11 = this.repository.getModDefinition("C", "1.1.0").get();
126 |
127 | this.installations.addInstallation(createInstallation(a13));
128 | this.installations.addInstallation(createInstallation(c11));
129 |
130 | Resolution resolution = this.resolver.resolve(b11);
131 |
132 | assertFalse(resolution.isErroneous());
133 | assertMatches(resolution.getInstall(), b11);
134 | assertMatches(resolution.getUninstall());
135 | }
136 |
137 | @Test
138 | public void update() {
139 | ModDefinition a1 = this.repository.getModDefinition("A", "1.0.0").get();
140 | ModDefinition a11 = this.repository.getModDefinition("A", "1.1.0").get();
141 |
142 | this.installations.addInstallation(createInstallation(a1));
143 |
144 | Resolution resolution = this.resolver.resolve(a11);
145 |
146 | assertFalse(resolution.isErroneous());
147 | assertMatches(resolution.getInstall(), a11);
148 | assertMatches(resolution.getUninstall(), a1);
149 | }
150 |
151 | @Test
152 | public void updateDependency() {
153 | ModDefinition a13 = this.repository.getModDefinition("A", "1.3.0").get();
154 | ModDefinition c11 = this.repository.getModDefinition("C", "1.1.0").get();
155 | ModDefinition c12 = this.repository.getModDefinition("C", "1.2.0").get();
156 |
157 | this.installations.addInstallation(createInstallation(a13));
158 | this.installations.addInstallation(createInstallation(c11));
159 |
160 | Resolution resolution = this.resolver.resolve(c12);
161 |
162 | assertFalse(resolution.isErroneous());
163 | assertMatches(resolution.getInstall(), c12);
164 | assertMatches(resolution.getUninstall(), c11);
165 | }
166 |
167 | @Test
168 | public void updateWithConflict() {
169 | ModDefinition a12 = this.repository.getModDefinition("A", "1.2.0").get();
170 | ModDefinition b10 = this.repository.getModDefinition("B", "1.0.0").get();
171 | ModDefinition b11 = this.repository.getModDefinition("B", "1.1.0").get();
172 | ModDefinition c10 = this.repository.getModDefinition("C", "1.0.0").get();
173 |
174 | this.installations.addInstallation(createInstallation(a12));
175 | this.installations.addInstallation(createInstallation(b10));
176 | this.installations.addInstallation(createInstallation(c10));
177 |
178 | Resolution resolution = this.resolver.resolve(b11);
179 |
180 | assertTrue(resolution.isErroneous());
181 | assertFalse(resolution.hasMissingDependencies());
182 | assertTrue(resolution.hasUnresolvableDependencies());
183 |
184 | assertMatches(resolution.getUnresolvableDependencies(), createDependency("C", "1.0.0"), createDependency("C", "^1.1.0"));
185 | }
186 |
187 | @Test
188 | public void updateWithDependencies() {
189 | ModDefinition a12 = this.repository.getModDefinition("A", "1.2.0").get();
190 | ModDefinition a13 = this.repository.getModDefinition("A", "1.3.0").get();
191 | ModDefinition c10 = this.repository.getModDefinition("C", "1.0.0").get();
192 | ModDefinition c13 = this.repository.getModDefinition("C", "1.3.0").get();
193 |
194 | this.installations.addInstallation(createInstallation(a12));
195 | this.installations.addInstallation(createInstallation(c10));
196 |
197 | Resolution resolution = this.resolver.resolve(a13);
198 |
199 | assertFalse(resolution.isErroneous());
200 | assertMatches(resolution.getInstall(), a13, c13);
201 | assertMatches(resolution.getUninstall(), a12, c10);
202 | }
203 |
204 | @Test
205 | public void updateWithDependencies2() {
206 | ModDefinition a12 = this.repository.getModDefinition("A", "1.2.0").get();
207 | ModDefinition a13 = this.repository.getModDefinition("A", "1.3.0").get();
208 | ModDefinition c1 = this.repository.getModDefinition("C", "1.0.0").get();
209 | ModDefinition c13 = this.repository.getModDefinition("C", "1.3.0").get();
210 |
211 | this.installations.addInstallation(createInstallation(a12));
212 | this.installations.addInstallation(createInstallation(c1));
213 |
214 | Resolution resolution = this.resolver.resolve(a13);
215 |
216 | assertFalse(resolution.isErroneous());
217 | assertMatches(resolution.getInstall(), a13, c13);
218 | assertMatches(resolution.getUninstall(), a12, c1);
219 | }
220 |
221 | @Test
222 | public void updateWithExistingDependencies() {
223 | ModDefinition b10 = this.repository.getModDefinition("B", "1.0.0").get();
224 | ModDefinition c11 = this.repository.getModDefinition("C", "1.1.0").get();
225 |
226 | this.installations.addInstallation(createInstallation(c11));
227 |
228 | Resolution resolution = this.resolver.resolve(b10);
229 |
230 | assertFalse(resolution.isErroneous());
231 | assertMatches(resolution.getInstall(), b10);
232 | assertMatches(resolution.getUninstall());
233 | }
234 |
235 | @Test
236 | public void updateWithExistingTransitiveDependencies() {
237 | ModDefinition a13 = this.repository.getModDefinition("A", "1.3.0").get();
238 | ModDefinition b10 = this.repository.getModDefinition("B", "1.0.0").get();
239 | ModDefinition c10 = this.repository.getModDefinition("C", "1.0.0").get();
240 | ModDefinition c13 = this.repository.getModDefinition("C", "1.3.0").get();
241 | ModDefinition d20 = this.repository.getModDefinition("D", "2.0.0").get();
242 |
243 | this.installations.addInstallation(createInstallation(a13));
244 | this.installations.addInstallation(createInstallation(b10));
245 | this.installations.addInstallation(createInstallation(c10));
246 |
247 | Resolution resolution = this.resolver.resolve(d20);
248 |
249 | assertFalse(resolution.isErroneous());
250 | assertMatches(resolution.getInstall(), d20, c13);
251 | assertMatches(resolution.getUninstall(), c10);
252 | }
253 |
254 | @Test
255 | public void updateWithMissingDefinition() {
256 | ModDefinition c10 = this.repository.getModDefinition("C", "1.0.0").get();
257 |
258 | ModDefinition c01 = new ModDefinition();
259 | c01.setName("C");
260 | c01.setVersion("0.1.0");
261 | this.installations.addInstallation(createInstallation(c01));
262 | Resolution resolution = this.resolver.resolve(c10);
263 |
264 | assertFalse(resolution.isErroneous());
265 | assertMatches(resolution.getInstall(), c10);
266 | assertMatches(resolution.getUninstall(), c01);
267 | }
268 |
269 | @Test
270 | public void updateWithTransitiveDependencies() {
271 | ModDefinition c13 = this.repository.getModDefinition("C", "1.3.0").get();
272 | ModDefinition d24 = this.repository.getModDefinition("D", "2.4.0").get();
273 | ModDefinition e20 = this.repository.getModDefinition("E", "2.0.0").get();
274 |
275 | Resolution resolution = this.resolver.resolve(e20);
276 |
277 | assertFalse(resolution.isErroneous());
278 | assertMatches(resolution.getInstall(), e20, d24, c13);
279 | assertMatches(resolution.getUninstall());
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/src/test/java/me/wulfmarius/modinstaller/VersionTest.java:
--------------------------------------------------------------------------------
1 | package me.wulfmarius.modinstaller;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import java.util.*;
6 |
7 | import org.junit.Test;
8 |
9 | public class VersionTest {
10 |
11 | @Test
12 | public void semverPreleaseFullVersion() {
13 | Version version = Version.parse("1.2.3-pre");
14 | assertEquals(1, version.getMajor());
15 | assertEquals(2, version.getMinor());
16 | assertEquals(3, version.getPatch());
17 | assertEquals("", version.getSpecial());
18 | assertEquals("pre", version.getPrelease());
19 | }
20 |
21 | @Test
22 | public void semverPreleaseMajorMinorVersion() {
23 | Version version = Version.parse("1.2-pre");
24 | assertEquals(1, version.getMajor());
25 | assertEquals(2, version.getMinor());
26 | assertEquals(0, version.getPatch());
27 | assertEquals("", version.getSpecial());
28 | assertEquals("pre", version.getPrelease());
29 | }
30 |
31 | @Test
32 | public void semverPreleaseMajorVersion() {
33 | Version version = Version.parse("1-pre");
34 | assertEquals(1, version.getMajor());
35 | assertEquals(0, version.getMinor());
36 | assertEquals(0, version.getPatch());
37 | assertEquals("", version.getSpecial());
38 | assertEquals("pre", version.getPrelease());
39 | }
40 |
41 | @Test
42 | public void semverSimpleFullVersion() {
43 | Version version = Version.parse("1.2.3");
44 | assertEquals(1, version.getMajor());
45 | assertEquals(2, version.getMinor());
46 | assertEquals(3, version.getPatch());
47 | assertEquals("", version.getSpecial());
48 | assertEquals("", version.getPrelease());
49 | }
50 |
51 | @Test
52 | public void semverSimpleMajorMinorVersion() {
53 | Version version = Version.parse("1.2");
54 | assertEquals(1, version.getMajor());
55 | assertEquals(2, version.getMinor());
56 | assertEquals(0, version.getPatch());
57 | assertEquals("", version.getSpecial());
58 | assertEquals("", version.getPrelease());
59 | }
60 |
61 | @Test
62 | public void semverSimpleMajorVersion() {
63 | Version version = Version.parse("1");
64 | assertEquals(1, version.getMajor());
65 | assertEquals(0, version.getMinor());
66 | assertEquals(0, version.getPatch());
67 | assertEquals("", version.getSpecial());
68 | assertEquals("", version.getPrelease());
69 | }
70 |
71 | @Test
72 | public void semverSorting() {
73 | Version version_0_5_0 = Version.parse("0.5");
74 | Version version_1_0_0 = Version.parse("1");
75 | Version version_1_0_1 = Version.parse("1.0.1");
76 | Version version_1_1_0_pre = Version.parse("1.1-pre");
77 | Version version_1_1_0 = Version.parse("1.1");
78 |
79 | List versions = Arrays.asList(version_0_5_0, version_1_0_0, version_1_0_1, version_1_1_0, version_1_1_0_pre);
80 | Collections.shuffle(versions);
81 | Collections.sort(versions, Version::compare);
82 |
83 | assertEquals(version_0_5_0, versions.get(0));
84 | assertEquals(version_1_0_0, versions.get(1));
85 | assertEquals(version_1_0_1, versions.get(2));
86 | assertEquals(version_1_1_0_pre, versions.get(3));
87 | assertEquals(version_1_1_0, versions.get(4));
88 | }
89 |
90 | @Test
91 | public void specialSorting() {
92 | Version version_1_5 = Version.parse("1.5");
93 | Version version_1_6c = Version.parse("1.6c");
94 | Version version_1_6e = Version.parse("1.6e");
95 | Version version_1_6f = Version.parse("1.6f");
96 | Version version_1_7 = Version.parse("1.7");
97 | Version version_1_7a = Version.parse("1.7a");
98 | Version version_17a = Version.parse("17a");
99 |
100 | List versions = Arrays.asList(version_1_5, version_1_6c, version_1_6e, version_1_6f, version_1_7, version_1_7a,
101 | version_17a);
102 | Collections.shuffle(versions);
103 | Collections.sort(versions, Version::compare);
104 |
105 | assertEquals(version_1_5, versions.get(0));
106 | assertEquals(version_1_6c, versions.get(1));
107 | assertEquals(version_1_6e, versions.get(2));
108 | assertEquals(version_1_6f, versions.get(3));
109 | assertEquals(version_1_7, versions.get(4));
110 | assertEquals(version_1_7a, versions.get(5));
111 | }
112 |
113 | @Test
114 | public void specialVersion() {
115 | Version version = Version.parse("1.7a");
116 | assertEquals(1, version.getMajor());
117 | assertEquals(7, version.getMinor());
118 | assertEquals(0, version.getPatch());
119 | assertEquals("a", version.getSpecial());
120 | assertEquals("", version.getPrelease());
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/test/resources/mod-a.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "A",
3 | "description": "Mod A",
4 | "url": "",
5 | "releases": [
6 | {
7 | "version": "1.0.0"
8 | }, {
9 | "version": "1.1.0"
10 | }, {
11 | "version": "1.2.0",
12 | "dependencies": [
13 | {"name": "C", "version": "1.0.0"}
14 | ]
15 | }, {
16 | "version": "1.3.0",
17 | "dependencies": [
18 | {"name": "C", "version": "^1.1.0"}
19 | ]
20 | }, {
21 | "version": "2.0.0"
22 | }, {
23 | "version": "2.1.0"
24 | }, {
25 | "version": "2.2.0"
26 | }, {
27 | "version": "2.3.0"
28 | }, {
29 | "version": "2.4.0"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/src/test/resources/mod-b.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "B",
3 | "description": "Mod B",
4 | "url": "",
5 | "releases": [
6 | {
7 | "version": "1.0.0",
8 | "dependencies": [
9 | {"name": "C", "version": "^1.0.0"}
10 | ]
11 | }, {
12 | "version": "1.1.0",
13 | "dependencies": [
14 | {"name": "C", "version": "^1.1.0"}
15 | ]
16 | }, {
17 | "version": "1.2.0"
18 | }, {
19 | "version": "1.3.0"
20 | }, {
21 | "version": "2.0.0"
22 | }, {
23 | "version": "2.1.0"
24 | }, {
25 | "version": "2.2.0"
26 | }, {
27 | "version": "2.3.0"
28 | }, {
29 | "version": "2.4.0"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/src/test/resources/mod-c.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "C",
3 | "description": "Mod C",
4 | "url": "",
5 | "releases": [
6 | {
7 | "version": "1.0.0"
8 | }, {
9 | "version": "1.1.0"
10 | }, {
11 | "version": "1.2.0"
12 | }, {
13 | "version": "1.3.0"
14 | }, {
15 | "version": "2.0.0"
16 | }, {
17 | "version": "2.1.0"
18 | }, {
19 | "version": "2.2.0"
20 | }, {
21 | "version": "2.3.0"
22 | }, {
23 | "version": "2.4.0"
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/src/test/resources/mod-d.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "D",
3 | "description": "Mod D",
4 | "url": "",
5 | "releases": [
6 | {
7 | "version": "1.0.0"
8 | }, {
9 | "version": "1.1.0"
10 | }, {
11 | "version": "1.2.0"
12 | }, {
13 | "version": "1.3.0"
14 | }, {
15 | "version": "2.0.0",
16 | "dependencies": [
17 | {"name": "C", "version": "^1.1.0"}
18 | ]
19 | }, {
20 | "version": "2.1.0",
21 | "dependencies": [
22 | {"name": "C", "version": "^1.1.0"}
23 | ]
24 | }, {
25 | "version": "2.2.0",
26 | "dependencies": [
27 | {"name": "C", "version": "^1.1.0"}
28 | ]
29 | }, {
30 | "version": "2.3.0",
31 | "dependencies": [
32 | {"name": "C", "version": "^1.1.0"}
33 | ]
34 | }, {
35 | "version": "2.4.0",
36 | "dependencies": [
37 | {"name": "C", "version": "^1.1.0"}
38 | ]
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/src/test/resources/mod-e.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "E",
3 | "description": "Mod E",
4 | "url": "",
5 | "releases": [
6 | {
7 | "version": "1.0.0"
8 | }, {
9 | "version": "1.1.0"
10 | }, {
11 | "version": "1.2.0"
12 | }, {
13 | "version": "1.3.0"
14 | }, {
15 | "version": "2.0.0",
16 | "dependencies": [
17 | {"name": "D", "version": "^2.0.0"}
18 | ]
19 | }, {
20 | "version": "2.1.0"
21 | }, {
22 | "version": "2.2.0"
23 | }, {
24 | "version": "2.3.0"
25 | }, {
26 | "version": "2.4.0"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/tld-versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "versions": [
3 | {
4 | "version": "v1.80",
5 | "date": "2020-06-30"
6 | }, {
7 | "version": "v1.41",
8 | "date": "2018-12-17"
9 | }, {
10 | "version": "v1.27",
11 | "date": "2018-03-21"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------